Skip to content

Commit 94c7b9d

Browse files
committed
feat: provider wrapper to support state race fix
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
1 parent 597501a commit 94c7b9d

File tree

3 files changed

+70
-10
lines changed

3 files changed

+70
-10
lines changed

src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
@Slf4j
99
class FeatureProviderStateManager implements EventProviderListener {
1010
private final FeatureProvider delegate;
11+
private final boolean delegateManagesState;
1112
private final AtomicBoolean isInitialized = new AtomicBoolean();
1213
private final AtomicReference<ProviderState> state = new AtomicReference<>(ProviderState.NOT_READY);
1314

1415
public FeatureProviderStateManager(FeatureProvider delegate) {
1516
this.delegate = delegate;
17+
this.delegateManagesState = delegate instanceof StateManagingProvider;
1618
if (delegate instanceof EventProvider) {
1719
((EventProvider) delegate).setEventProviderListener(this);
1820
}
@@ -24,30 +26,41 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
2426
}
2527
try {
2628
delegate.initialize(evaluationContext);
27-
setState(ProviderState.READY);
29+
if (!delegateManagesState) {
30+
setState(ProviderState.READY);
31+
}
2832
} catch (OpenFeatureError openFeatureError) {
29-
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
30-
setState(ProviderState.FATAL);
31-
} else {
32-
setState(ProviderState.ERROR);
33+
if (!delegateManagesState) {
34+
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
35+
setState(ProviderState.FATAL);
36+
} else {
37+
setState(ProviderState.ERROR);
38+
}
3339
}
3440
isInitialized.set(false);
3541
throw openFeatureError;
3642
} catch (Exception e) {
37-
setState(ProviderState.ERROR);
43+
if (!delegateManagesState) {
44+
setState(ProviderState.ERROR);
45+
}
3846
isInitialized.set(false);
3947
throw e;
4048
}
4149
}
4250

4351
public void shutdown() {
4452
delegate.shutdown();
45-
setState(ProviderState.NOT_READY);
53+
if (!delegateManagesState) {
54+
setState(ProviderState.NOT_READY);
55+
}
4656
isInitialized.set(false);
4757
}
4858

4959
@Override
5060
public void onEmit(ProviderEvent event, ProviderEventDetails details) {
61+
if (delegateManagesState) {
62+
return;
63+
}
5164
if (ProviderEvent.PROVIDER_ERROR.equals(event)) {
5265
if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) {
5366
setState(ProviderState.FATAL);
@@ -75,13 +88,23 @@ private void setState(ProviderState state) {
7588
}
7689

7790
public ProviderState getState() {
91+
if (delegateManagesState) {
92+
return delegate.getState();
93+
}
7894
return state.get();
7995
}
8096

8197
FeatureProvider getProvider() {
8298
return delegate;
8399
}
84100

101+
/**
102+
* Returns true if the delegate provider manages its own state.
103+
*/
104+
boolean delegateManagesState() {
105+
return delegateManagesState;
106+
}
107+
85108
public boolean hasSameProvider(FeatureProvider featureProvider) {
86109
return this.delegate.equals(featureProvider);
87110
}

src/main/java/dev/openfeature/sdk/ProviderRepository.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,21 +215,30 @@ private void initializeProvider(
215215
try {
216216
if (ProviderState.NOT_READY.equals(newManager.getState())) {
217217
newManager.initialize(openFeatureAPI.getEvaluationContext());
218-
afterInit.accept(newManager.getProvider());
218+
// State-managing providers emit their own events; skip SDK-side emission.
219+
if (!newManager.delegateManagesState()) {
220+
afterInit.accept(newManager.getProvider());
221+
}
219222
}
220223
shutDownOld(oldManager, afterShutdown);
221224
} catch (OpenFeatureError e) {
222225
log.error(
223226
"Exception when initializing feature provider {}",
224227
newManager.getProvider().getClass().getName(),
225228
e);
226-
afterError.accept(newManager.getProvider(), e);
229+
// State-managing providers emit their own events; skip SDK-side emission.
230+
if (!newManager.delegateManagesState()) {
231+
afterError.accept(newManager.getProvider(), e);
232+
}
227233
} catch (Exception e) {
228234
log.error(
229235
"Exception when initializing feature provider {}",
230236
newManager.getProvider().getClass().getName(),
231237
e);
232-
afterError.accept(newManager.getProvider(), new GeneralError(e));
238+
// State-managing providers emit their own events; skip SDK-side emission.
239+
if (!newManager.delegateManagesState()) {
240+
afterError.accept(newManager.getProvider(), new GeneralError(e));
241+
}
233242
}
234243
}
235244

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* A provider that manages its own state. The SDK reads state from the provider
5+
* rather than maintaining shadow state. Implementations MUST ensure that
6+
* {@link #getState()} is safe for concurrent access and that state transitions
7+
* and associated event emissions are atomic from the perspective of external observers.
8+
*
9+
* <p>Legacy providers that do not implement this interface continue to have their state
10+
* managed by the SDK (deprecated behavior, to be removed in the next major version).</p>
11+
*
12+
* @see FeatureProvider
13+
* @see EventProvider
14+
*/
15+
public interface StateManagingProvider extends FeatureProvider {
16+
17+
/**
18+
* Returns the current state of this provider. Must reflect {@link ProviderState#NOT_READY}
19+
* before {@link #initialize(EvaluationContext)} is called and after {@link #shutdown()} completes.
20+
* Must reflect {@link ProviderState#READY} if {@link #initialize(EvaluationContext)} returns normally.
21+
*
22+
* <p>This method must be safe for concurrent access.</p>
23+
*
24+
* @return the current provider state
25+
*/
26+
@Override
27+
ProviderState getState();
28+
}

0 commit comments

Comments
 (0)