diff --git a/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java index 5fd70221b..f05525361 100644 --- a/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java +++ b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java @@ -8,11 +8,13 @@ @Slf4j class FeatureProviderStateManager implements EventProviderListener { private final FeatureProvider delegate; + private final boolean delegateManagesState; private final AtomicBoolean isInitialized = new AtomicBoolean(); private final AtomicReference state = new AtomicReference<>(ProviderState.NOT_READY); public FeatureProviderStateManager(FeatureProvider delegate) { this.delegate = delegate; + this.delegateManagesState = delegate instanceof StateManagingProvider; if (delegate instanceof EventProvider) { ((EventProvider) delegate).setEventProviderListener(this); } @@ -24,17 +26,23 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } try { delegate.initialize(evaluationContext); - setState(ProviderState.READY); + if (!delegateManagesState) { + setState(ProviderState.READY); + } } catch (OpenFeatureError openFeatureError) { - if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) { - setState(ProviderState.FATAL); - } else { - setState(ProviderState.ERROR); + if (!delegateManagesState) { + if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) { + setState(ProviderState.FATAL); + } else { + setState(ProviderState.ERROR); + } } isInitialized.set(false); throw openFeatureError; } catch (Exception e) { - setState(ProviderState.ERROR); + if (!delegateManagesState) { + setState(ProviderState.ERROR); + } isInitialized.set(false); throw e; } @@ -42,12 +50,17 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { public void shutdown() { delegate.shutdown(); - setState(ProviderState.NOT_READY); + if (!delegateManagesState) { + setState(ProviderState.NOT_READY); + } isInitialized.set(false); } @Override public void onEmit(ProviderEvent event, ProviderEventDetails details) { + if (delegateManagesState) { + return; + } if (ProviderEvent.PROVIDER_ERROR.equals(event)) { if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) { setState(ProviderState.FATAL); @@ -75,6 +88,9 @@ private void setState(ProviderState state) { } public ProviderState getState() { + if (delegateManagesState) { + return delegate.getState(); + } return state.get(); } @@ -82,6 +98,13 @@ FeatureProvider getProvider() { return delegate; } + /** + * Returns true if the delegate provider manages its own state. + */ + boolean delegateManagesState() { + return delegateManagesState; + } + public boolean hasSameProvider(FeatureProvider featureProvider) { return this.delegate.equals(featureProvider); } diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index 3a0d325df..5d45c31f0 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -215,7 +215,10 @@ private void initializeProvider( try { if (ProviderState.NOT_READY.equals(newManager.getState())) { newManager.initialize(openFeatureAPI.getEvaluationContext()); - afterInit.accept(newManager.getProvider()); + // State-managing providers emit their own events; skip SDK-side emission. + if (!newManager.delegateManagesState()) { + afterInit.accept(newManager.getProvider()); + } } shutDownOld(oldManager, afterShutdown); } catch (OpenFeatureError e) { @@ -223,13 +226,19 @@ private void initializeProvider( "Exception when initializing feature provider {}", newManager.getProvider().getClass().getName(), e); - afterError.accept(newManager.getProvider(), e); + // State-managing providers emit their own events; skip SDK-side emission. + if (!newManager.delegateManagesState()) { + afterError.accept(newManager.getProvider(), e); + } } catch (Exception e) { log.error( "Exception when initializing feature provider {}", newManager.getProvider().getClass().getName(), e); - afterError.accept(newManager.getProvider(), new GeneralError(e)); + // State-managing providers emit their own events; skip SDK-side emission. + if (!newManager.delegateManagesState()) { + afterError.accept(newManager.getProvider(), new GeneralError(e)); + } } } diff --git a/src/main/java/dev/openfeature/sdk/StateManagingProvider.java b/src/main/java/dev/openfeature/sdk/StateManagingProvider.java new file mode 100644 index 000000000..fe5c8459f --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/StateManagingProvider.java @@ -0,0 +1,28 @@ +package dev.openfeature.sdk; + +/** + * A provider that manages its own state. The SDK reads state from the provider + * rather than maintaining shadow state. Implementations MUST ensure that + * {@link #getState()} is safe for concurrent access and that state transitions + * and associated event emissions are atomic from the perspective of external observers. + * + *

Legacy providers that do not implement this interface continue to have their state + * managed by the SDK (deprecated behavior, to be removed in the next major version).

+ * + * @see FeatureProvider + * @see EventProvider + */ +public interface StateManagingProvider extends FeatureProvider { + + /** + * Returns the current state of this provider. Must reflect {@link ProviderState#NOT_READY} + * before {@link #initialize(EvaluationContext)} is called and after {@link #shutdown()} completes. + * Must reflect {@link ProviderState#READY} if {@link #initialize(EvaluationContext)} returns normally. + * + *

This method must be safe for concurrent access.

+ * + * @return the current provider state + */ + @Override + ProviderState getState(); +}