Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Config Resolver #459

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions polaris-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ featureConfiguration:
- AZURE
- FILE

dynamicFeatureConfigResolver:
type: no-op

callContextResolver:
type: default

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,33 @@
public class DefaultConfigurationStore implements PolarisConfigurationStore {
private final Map<String, Object> config;
private final Map<String, Map<String, Object>> realmConfig;
private final DynamicFeatureConfigResolver dynamicFeatureConfigResolver;

public DefaultConfigurationStore(Map<String, Object> config) {
this.config = config;
this.realmConfig = Map.of();
this.dynamicFeatureConfigResolver = new NoOpDynamicFeatureConfigResolver();
}

public DefaultConfigurationStore(
Map<String, Object> config, Map<String, Map<String, Object>> realmConfig) {
Map<String, Object> config,
Map<String, Map<String, Object>> realmConfig,
DynamicFeatureConfigResolver dynamicFeatureConfigResolver) {
this.config = config;
this.realmConfig = realmConfig;
this.dynamicFeatureConfigResolver = dynamicFeatureConfigResolver;
}

@SuppressWarnings("unchecked")
@Override
public <T> @Nullable T getConfiguration(@NotNull PolarisCallContext ctx, String configName) {
String realm = CallContext.getCurrentContext().getRealmContext().getRealmIdentifier();
return (T)
realmConfig.getOrDefault(realm, Map.of()).getOrDefault(configName, config.get(configName));
dynamicFeatureConfigResolver
.resolve(configName)
.orElse(
realmConfig
.getOrDefault(realm, Map.of())
.getOrDefault(configName, config.get(configName)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.service.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.dropwizard.jackson.Discoverable;
import java.util.Optional;

/**
* DynamicFeatureConfigResolvers dynamically resolve featureConfigurations. This is useful for
* integration with feature flag systems which are intended for fetching configs at runtime.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
public interface DynamicFeatureConfigResolver extends Discoverable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, I feel that this type should be unnecessary. Unfortunately, we just added the featureConfiguration field as a map, rather than having the DefaultConfigurationStore instantiated and populated by Jackson during application startup. Now the configuration can't really change to add, e.g., a type configuration key so that we could use different PolarisConfigurationStore implementations. The PolarisConfigurationStore interface was always intended to support dynamic implementations - which is why the PolarisCallContext is part of the signature.

I'm just trying to figure out if there isn't a way to make the implementation swappable, while still supporting the backward-compatible configuration. I mean, there's no real harm in adding another interface, but it just feels unnecessary and the consequence of poor decision making earlier on. I hate the idea of that poor decision making having to follow us around forever.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to make the whole implementation swappable as you mentioned, which could be implemented in a backward compatible way: if a custom type is specified, use it, otherwise use the default. I don't think this change makes that impossible in the future. In fact, you can simply make DynamicFeatureConfigResolver implement the entire configuration store.

/**
* Resolves a dynamic config by its key name.
*
* @param key
* @return The config value or Optional.empty() if the config should not be dynamically resolved.
* If it's not dynamically resolved, it will be deferred to the application config.
*/
Optional<Object> resolve(String key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.service.config;

import com.fasterxml.jackson.annotation.JsonTypeName;
import java.util.Optional;

/** An empty dynamic config resolver. */
@JsonTypeName("no-op")
public class NoOpDynamicFeatureConfigResolver implements DynamicFeatureConfigResolver {
@Override
public Optional<Object> resolve(String key) {
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class PolarisApplicationConfig extends Configuration {
private String awsSecretKey;
private FileIOFactory fileIOFactory;
private RateLimiter rateLimiter;
private DynamicFeatureConfigResolver dynamicFeatureConfigResolver;

private AccessToken gcpAccessToken;

Expand All @@ -89,6 +90,17 @@ public FileIOFactory getFileIOFactory() {
return fileIOFactory;
}

@JsonProperty("dynamicFeatureConfigResolver")
public void setDynamicFeatureConfigResolver(
DynamicFeatureConfigResolver dynamicFeatureConfigResolver) {
this.dynamicFeatureConfigResolver = dynamicFeatureConfigResolver;
}

@JsonProperty("dynamicFeatureConfigResolver")
public DynamicFeatureConfigResolver getDynamicFeatureConfigResolver() {
return dynamicFeatureConfigResolver;
}

@JsonProperty("authenticator")
public void setPolarisAuthenticator(
DiscoverableAuthenticator<String, AuthenticatedPolarisPrincipal> polarisAuthenticator) {
Expand Down Expand Up @@ -194,7 +206,8 @@ public long getMaxRequestBodyBytes() {
}

public PolarisConfigurationStore getConfigurationStore() {
return new DefaultConfigurationStore(globalFeatureConfiguration, realmConfiguration);
return new DefaultConfigurationStore(
globalFeatureConfiguration, realmConfiguration, dynamicFeatureConfigResolver);
}

public List<String> getDefaultRealms() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

org.apache.polaris.service.auth.DiscoverableAuthenticator
org.apache.polaris.core.persistence.MetaStoreManagerFactory
org.apache.polaris.service.config.DynamicFeatureConfigResolver
org.apache.polaris.service.config.OAuth2ApiService
org.apache.polaris.service.context.RealmContextResolver
org.apache.polaris.service.context.CallContextResolver
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#

org.apache.polaris.service.config.NoOpDynamicFeatureConfigResolver
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@

import static org.assertj.core.api.Assertions.assertThat;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

public class DefaultConfigurationStoreTest {

@Test
public void testGetConfiguration() {
DefaultConfigurationStore defaultConfigurationStore =
Expand Down Expand Up @@ -65,7 +69,8 @@ public void testGetRealmConfiguration() {
"realm1",
Map.of("key1", realm1KeyOneValue),
"realm2",
Map.of("key1", realm2KeyOneValue, "key2", realm2KeyTwoValue)));
Map.of("key1", realm2KeyOneValue, "key2", realm2KeyTwoValue)),
new NoOpDynamicFeatureConfigResolver());
InMemoryPolarisMetaStoreManagerFactory metastoreFactory =
new InMemoryPolarisMetaStoreManagerFactory();

Expand Down Expand Up @@ -106,4 +111,77 @@ public void testGetRealmConfiguration() {
String keyTwoRealm3 = defaultConfigurationStore.getConfiguration(realm3Ctx, "key2");
assertThat(keyTwoRealm3).isEqualTo(defaultKeyTwoValue);
}

@Test
public void testDynamicConfig() {
InMemoryPolarisMetaStoreManagerFactory metastoreFactory =
new InMemoryPolarisMetaStoreManagerFactory();
PolarisCallContext polarisCtx =
new PolarisCallContext(
metastoreFactory.getOrCreateSessionSupplier(() -> "realm1").get(),
new PolarisDefaultDiagServiceImpl());

String key = "k1";
Map<String, Object> staticConfig = Map.of(key, 10);

assertThat(
new DefaultConfigurationStore(staticConfig, Map.of(), k -> Optional.empty())
.<Integer>getConfiguration(polarisCtx, key))
.as("The DynamicFeatureConfigResolver always returns Optional.empty()")
.isEqualTo(10);

assertThat(
new DefaultConfigurationStore(staticConfig, Map.of(), k -> Optional.of(5))
.<Integer>getConfiguration(polarisCtx, key))
.as("The DynamicFeatureConfigResolver always returns 5")
.isEqualTo(5);
}

@ParameterizedTest
@MethodSource("getTestConfigs")
public void testPrecedenceIsDynamicThenStaticPerRealmThenStaticGlobal(TestConfig testConfig) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about catalog-level? I'd assume the catalog-level properties take precedence over everything, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this it already should. I assumed there were tests already for that.

InMemoryPolarisMetaStoreManagerFactory metastoreFactory =
new InMemoryPolarisMetaStoreManagerFactory();

String realm = "realm1";
PolarisCallContext polarisCtx =
new PolarisCallContext(
metastoreFactory.getOrCreateSessionSupplier(() -> realm).get(),
new PolarisDefaultDiagServiceImpl());

String key = "k1";

Map<String, Object> staticConfig = new HashMap<>();
if (testConfig.staticConfig != null) {
staticConfig.put(key, testConfig.staticConfig);
}

Map<String, Object> realmConfig = new HashMap<>();
if (testConfig.realmConfig != null) {
realmConfig.put(key, testConfig.realmConfig);
}

DefaultConfigurationStore configStore =
new DefaultConfigurationStore(
staticConfig,
Map.of(realm, realmConfig),
(k) -> Optional.ofNullable(testConfig.dynamicConfig));
assertThat(configStore.<Integer>getConfiguration(polarisCtx, key))
.isEqualTo(testConfig.expectedValue);
}

private static Stream<TestConfig> getTestConfigs() {
return Stream.of(
new TestConfig(null, null, null, null),
new TestConfig(5, null, null, 5),
new TestConfig(5, 6, null, 6),
new TestConfig(5, 6, 7, 7),
new TestConfig(5, null, 7, 7),
new TestConfig(null, null, 7, 7),
new TestConfig(null, 6, 7, 7),
new TestConfig(null, 6, null, 6));
}

public record TestConfig(
Integer staticConfig, Integer realmConfig, Integer dynamicConfig, Integer expectedValue) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ featureConfiguration:
- GCS
- AZURE

dynamicFeatureConfigResolver:
type: no-op

metaStoreManager:
type: in-memory

Expand Down