Skip to content
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
33 changes: 33 additions & 0 deletions src/main/java/dev/openfeature/sdk/FeatureProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* The interface implemented by upstream flag providers to resolve flags for
Expand Down Expand Up @@ -41,6 +42,38 @@ default void initialize(EvaluationContext evaluationContext) throws Exception {
// Intentionally left blank
}

/**
* This method is called before a provider is used to evaluate flags, with the
* bound domain supplied when the provider is registered to a named client.
*
* <p>
* The default provider is initialized with a {@code null} domain. Providers that
* maintain per-domain state (for example a persistent cache) should override this
* method and declare themselves {@linkplain #isDomainScoped() domain-scoped}.
* </p>
*
* @param evaluationContext the global evaluation context
* @param domain the bound domain, or {@code null} for the default provider
*/
default void initialize(EvaluationContext evaluationContext, @Nullable String domain) throws Exception {
initialize(evaluationContext);
}

/**
* Returns whether this provider maintains state specific to a single domain that
* cannot be shared across domains.
*
* <p>
* Domain-scoped providers may only be bound to one domain within a single API
* instance.
* </p>
*
* @return {@code true} if this provider is domain-scoped
*/
default boolean isDomainScoped() {
return false;
}

/**
* This method is called when a new provider is about to be used to evaluate
* flags, or the SDK is shut down.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;

@Slf4j
Expand All @@ -19,11 +20,15 @@ public FeatureProviderStateManager(FeatureProvider delegate) {
}

public void initialize(EvaluationContext evaluationContext) throws Exception {
initialize(evaluationContext, null);
}

public void initialize(EvaluationContext evaluationContext, @Nullable String domain) throws Exception {
if (isInitialized.getAndSet(true)) {
return;
}
try {
delegate.initialize(evaluationContext);
delegate.initialize(evaluationContext, domain);
setState(ProviderState.READY);
} catch (OpenFeatureError openFeatureError) {
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
Expand Down
59 changes: 53 additions & 6 deletions src/main/java/dev/openfeature/sdk/ProviderRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;

@Slf4j
Expand Down Expand Up @@ -154,7 +155,7 @@ public void setProvider(
}

private void prepareAndInitializeProvider(
String domain,
@Nullable String domain,
FeatureProvider newProvider,
Consumer<FeatureProvider> afterSet,
Consumer<FeatureProvider> afterInit,
Expand All @@ -169,6 +170,7 @@ private void prepareAndInitializeProvider(
throw new IllegalStateException("Provider cannot be set while repository is shutting down");
}
FeatureProviderStateManager existing = getExistingStateManagerForProvider(newProvider);
validateDomainScopedBinding(domain, newProvider);
if (existing == null) {
openFeatureAPI.registerGlobalProvider(newProvider);
newStateManager = new FeatureProviderStateManager(newProvider);
Expand All @@ -185,37 +187,82 @@ private void prepareAndInitializeProvider(
}

if (waitForInit) {
initializeProvider(newStateManager, afterInit, afterShutdown, afterError, oldStateManager);
initializeProvider(domain, newStateManager, afterInit, afterShutdown, afterError, oldStateManager);
} else {
taskExecutor.submit(() -> {
// initialization happens in a different thread if we're not waiting for it
initializeProvider(newStateManager, afterInit, afterShutdown, afterError, oldStateManager);
initializeProvider(domain, newStateManager, afterInit, afterShutdown, afterError, oldStateManager);
});
}
}

private void validateDomainScopedBinding(@Nullable String domain, FeatureProvider newProvider) {
if (!newProvider.isDomainScoped()) {
return;
}

boolean currentlyDefault = isDefaultProviderInstance(newProvider);
List<String> boundNamedDomains = getBoundDomainsForProviderInstance(newProvider);

if (!currentlyDefault && boundNamedDomains.isEmpty()) {
return;
}

if (domain == null) {
if (!currentlyDefault) {
throw new IllegalArgumentException("Domain-scoped provider cannot be bound to more than one domain");
}
return;
}
if (boundNamedDomains.contains(domain)) {
return;
}
if (!boundNamedDomains.isEmpty() || currentlyDefault) {
throw new IllegalArgumentException("Domain-scoped provider cannot be bound to more than one domain");
}
}

private boolean isDefaultProviderInstance(FeatureProvider provider) {
return defaultStateManger.get().getProvider() == provider;
}

private List<String> getBoundDomainsForProviderInstance(FeatureProvider provider) {
return stateManagers.entrySet().stream()
.filter(entry -> entry.getValue().getProvider() == provider)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}

private FeatureProviderStateManager getExistingStateManagerForProvider(FeatureProvider provider) {
for (FeatureProviderStateManager stateManager : stateManagers.values()) {
if (stateManager.hasSameProvider(provider)) {
if (matchesProvider(stateManager.getProvider(), provider)) {
return stateManager;
}
}
FeatureProviderStateManager defaultFeatureProviderStateManager = defaultStateManger.get();
if (defaultFeatureProviderStateManager.hasSameProvider(provider)) {
if (matchesProvider(defaultFeatureProviderStateManager.getProvider(), provider)) {
return defaultFeatureProviderStateManager;
}
return null;
}

private boolean matchesProvider(FeatureProvider registered, FeatureProvider candidate) {
if (candidate.isDomainScoped()) {
return registered == candidate;
}
return registered.equals(candidate);
}

private void initializeProvider(
@Nullable String domain,
FeatureProviderStateManager newManager,
Consumer<FeatureProvider> afterInit,
Consumer<FeatureProvider> afterShutdown,
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
FeatureProviderStateManager oldManager) {
try {
if (ProviderState.NOT_READY.equals(newManager.getState())) {
newManager.initialize(openFeatureAPI.getEvaluationContext());
newManager.initialize(openFeatureAPI.getEvaluationContext(), domain);
afterInit.accept(newManager.getProvider());
}
shutDownOld(oldManager, afterShutdown);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -84,6 +85,19 @@ protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvide
*/
@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
initialize(evaluationContext, null);
}

/**
* Initialize the provider with the bound domain, if any.
*
* @param evaluationContext evaluation context
* @param domain the bound domain, or {@code null} for the default provider
* @throws Exception on error (e.g. wrapped {@link java.util.concurrent.ExecutionException}
* from a failing provider)
*/
@Override
public void initialize(EvaluationContext evaluationContext, @Nullable String domain) throws Exception {
var metadataBuilder = MultiProviderMetadata.builder().name(NAME);
HashMap<String, Metadata> providersMetadata = new HashMap<>();

Expand All @@ -98,7 +112,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
Collection<Callable<Void>> tasks = new ArrayList<>(providers.size());
for (FeatureProvider provider : providers.values()) {
tasks.add(() -> {
provider.initialize(evaluationContext);
provider.initialize(evaluationContext, domain);
return null;
Comment thread
jonathannorris marked this conversation as resolved.
});
Metadata providerMetadata = provider.getMetadata();
Expand Down
Loading
Loading