Skip to content

Commit 1b1e527

Browse files
chrfwowtoddbaert
andauthored
feat: make provider interface "stateless"; SDK maintains provider state (#1096)
* Make provider interface "stateless", SDK maintains provider state Signed-off-by: christian.lutnik <[email protected]> Signed-off-by: Todd Baert <[email protected]> Co-authored-by: christian.lutnik <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent dd8ba81 commit 1b1e527

28 files changed

+1070
-580
lines changed

README.md

+7-10
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,6 @@ public class MyProvider implements FeatureProvider {
317317
return () -> "My Provider";
318318
}
319319

320-
@Override
321-
public ProviderState getState() {
322-
// optionally indicate your provider's state (assumed to be READY if not implemented)
323-
}
324-
325320
@Override
326321
public void initialize(EvaluationContext evaluationContext) throws Exception {
327322
// start up your provider
@@ -368,11 +363,6 @@ class MyEventProvider extends EventProvider {
368363
return () -> "My Event Provider";
369364
}
370365

371-
@Override
372-
public ProviderState getState() {
373-
// indicate your provider's state (required for EventProviders)
374-
}
375-
376366
@Override
377367
public void initialize(EvaluationContext evaluationContext) throws Exception {
378368
// emit events when flags are changed in a hypothetical REST API
@@ -391,6 +381,13 @@ class MyEventProvider extends EventProvider {
391381
}
392382
```
393383

384+
Providers no longer need to manage their own state, this is done by the SDK itself. If desired, the state of a provider
385+
can be queried through the client that uses the provider.
386+
387+
```java
388+
OpenFeatureAPI.getInstance().getClient().getProviderState();
389+
```
390+
394391
> Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs!
395392
396393
### Develop a hook

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

+6
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@ public interface Client extends Features, EventBus<Client> {
3333
* @return A list of {@link Hook}s.
3434
*/
3535
List<Hook> getHooks();
36+
37+
/**
38+
* Returns the current state of the associated provider.
39+
* @return the provider state
40+
*/
41+
ProviderState getProviderState();
3642
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
@SuppressWarnings("checkstyle:MissingJavadocType")
44
public enum ErrorCode {
5-
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL
5+
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL,
6+
PROVIDER_FATAL
67
}

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

+14-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dev.openfeature.sdk.internal.TriConsumer;
44

5+
56
/**
67
* Abstract EventProvider. Providers must extend this class to support events.
78
* Emit events with {@link #emit(ProviderEvent, ProviderEventDetails)}. Please
@@ -15,22 +16,20 @@
1516
* @see FeatureProvider
1617
*/
1718
public abstract class EventProvider implements FeatureProvider {
19+
private EventProviderListener eventProviderListener;
1820

19-
/**
20-
* {@inheritDoc}
21-
*/
22-
@Override
23-
public abstract ProviderState getState();
21+
void setEventProviderListener(EventProviderListener eventProviderListener) {
22+
this.eventProviderListener = eventProviderListener;
23+
}
2424

2525
private TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit = null;
2626

2727
/**
2828
* "Attach" this EventProvider to an SDK, which allows events to propagate from this provider.
29-
* No-op if the same onEmit is already attached.
29+
* No-op if the same onEmit is already attached.
3030
*
3131
* @param onEmit the function to run when a provider emits events.
3232
* @throws IllegalStateException if attempted to bind a new emitter for already bound provider
33-
*
3433
*/
3534
void attach(TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit) {
3635
if (this.onEmit != null && this.onEmit != onEmit) {
@@ -50,11 +49,14 @@ void detach() {
5049

5150
/**
5251
* Emit the specified {@link ProviderEvent}.
53-
*
52+
*
5453
* @param event The event type
5554
* @param details The details of the event
5655
*/
5756
public void emit(ProviderEvent event, ProviderEventDetails details) {
57+
if (eventProviderListener != null) {
58+
eventProviderListener.onEmit(event, details);
59+
}
5860
if (this.onEmit != null) {
5961
this.onEmit.accept(this, event, details);
6062
}
@@ -63,7 +65,7 @@ public void emit(ProviderEvent event, ProviderEventDetails details) {
6365
/**
6466
* Emit a {@link ProviderEvent#PROVIDER_READY} event.
6567
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
66-
*
68+
*
6769
* @param details The details of the event
6870
*/
6971
public void emitProviderReady(ProviderEventDetails details) {
@@ -74,7 +76,7 @@ public void emitProviderReady(ProviderEventDetails details) {
7476
* Emit a
7577
* {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED}
7678
* event. Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
77-
*
79+
*
7880
* @param details The details of the event
7981
*/
8082
public void emitProviderConfigurationChanged(ProviderEventDetails details) {
@@ -84,7 +86,7 @@ public void emitProviderConfigurationChanged(ProviderEventDetails details) {
8486
/**
8587
* Emit a {@link ProviderEvent#PROVIDER_STALE} event.
8688
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
87-
*
89+
*
8890
* @param details The details of the event
8991
*/
9092
public void emitProviderStale(ProviderEventDetails details) {
@@ -94,7 +96,7 @@ public void emitProviderStale(ProviderEventDetails details) {
9496
/**
9597
* Emit a {@link ProviderEvent#PROVIDER_ERROR} event.
9698
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
97-
*
99+
*
98100
* @param details The details of the event
99101
*/
100102
public void emitProviderError(ProviderEventDetails details) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dev.openfeature.sdk;
2+
3+
@FunctionalInterface
4+
interface EventProviderListener {
5+
void onEmit(ProviderEvent event, ProviderEventDetails details);
6+
}

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ default void shutdown() {
6060
* If the provider needs to be initialized, it should return {@link ProviderState#NOT_READY}.
6161
* If the provider is in an error state, it should return {@link ProviderState#ERROR}.
6262
* If the provider is functioning normally, it should return {@link ProviderState#READY}.
63-
*
63+
*
6464
* <p><i>Providers which do not implement this method are assumed to be ready immediately.</i></p>
65-
*
65+
*
6666
* @return ProviderState
67+
* @deprecated The state is handled by the SDK internally. Query the state from the {@link Client} instead.
6768
*/
69+
@Deprecated
6870
default ProviderState getState() {
6971
return ProviderState.READY;
7072
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package dev.openfeature.sdk;
2+
3+
import dev.openfeature.sdk.exceptions.OpenFeatureError;
4+
import lombok.Getter;
5+
6+
import java.util.concurrent.atomic.AtomicBoolean;
7+
8+
class FeatureProviderStateManager implements EventProviderListener {
9+
private final FeatureProvider delegate;
10+
private final AtomicBoolean isInitialized = new AtomicBoolean();
11+
@Getter
12+
private ProviderState state = ProviderState.NOT_READY;
13+
14+
public FeatureProviderStateManager(FeatureProvider delegate) {
15+
this.delegate = delegate;
16+
if (delegate instanceof EventProvider) {
17+
((EventProvider) delegate).setEventProviderListener(this);
18+
}
19+
}
20+
21+
public void initialize(EvaluationContext evaluationContext) throws Exception {
22+
if (isInitialized.getAndSet(true)) {
23+
return;
24+
}
25+
try {
26+
delegate.initialize(evaluationContext);
27+
state = ProviderState.READY;
28+
} catch (OpenFeatureError openFeatureError) {
29+
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
30+
state = ProviderState.FATAL;
31+
} else {
32+
state = ProviderState.ERROR;
33+
}
34+
isInitialized.set(false);
35+
throw openFeatureError;
36+
} catch (Exception e) {
37+
state = ProviderState.ERROR;
38+
isInitialized.set(false);
39+
throw e;
40+
}
41+
}
42+
43+
public void shutdown() {
44+
delegate.shutdown();
45+
state = ProviderState.NOT_READY;
46+
isInitialized.set(false);
47+
}
48+
49+
@Override
50+
public void onEmit(ProviderEvent event, ProviderEventDetails details) {
51+
if (ProviderEvent.PROVIDER_ERROR.equals(event)) {
52+
if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) {
53+
state = ProviderState.FATAL;
54+
} else {
55+
state = ProviderState.ERROR;
56+
}
57+
} else if (ProviderEvent.PROVIDER_STALE.equals(event)) {
58+
state = ProviderState.STALE;
59+
} else if (ProviderEvent.PROVIDER_READY.equals(event)) {
60+
state = ProviderState.READY;
61+
}
62+
}
63+
64+
FeatureProvider getProvider() {
65+
return delegate;
66+
}
67+
68+
public boolean hasSameProvider(FeatureProvider featureProvider) {
69+
return this.delegate.equals(featureProvider);
70+
}
71+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double default
5959

6060
@Override
6161
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue,
62-
EvaluationContext invocationContext) {
62+
EvaluationContext invocationContext) {
6363
return ProviderEvaluation.<Value>builder()
6464
.value(defaultValue)
6565
.variant(PASSED_IN_DEFAULT)

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

+22-19
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
66
import lombok.extern.slf4j.Slf4j;
77

8-
import java.util.ArrayList;
9-
import java.util.Arrays;
10-
import java.util.List;
11-
import java.util.Optional;
12-
import java.util.Set;
8+
import java.util.*;
139
import java.util.function.Consumer;
1410

1511
/**
@@ -69,7 +65,7 @@ public Metadata getProviderMetadata(String domain) {
6965
}
7066

7167
/**
72-
* A factory function for creating new, OpenFeature clients.
68+
* A factory function for creating new, OpenFeature client.
7369
* Clients can contain their own state (e.g. logger, hook, context).
7470
* Multiple clients can be used to segment feature flag configuration.
7571
* All un-named or unbound clients use the default provider.
@@ -81,12 +77,12 @@ public Client getClient() {
8177
}
8278

8379
/**
84-
* A factory function for creating new domainless OpenFeature clients.
80+
* A factory function for creating new domainless OpenFeature client.
8581
* Clients can contain their own state (e.g. logger, hook, context).
8682
* Multiple clients can be used to segment feature flag configuration.
8783
* If there is already a provider bound to this domain, this provider will be used.
8884
* Otherwise, the default provider is used until a provider is assigned to that domain.
89-
*
85+
*
9086
* @param domain an identifier which logically binds clients with providers
9187
* @return a new client instance
9288
*/
@@ -95,20 +91,22 @@ public Client getClient(String domain) {
9591
}
9692

9793
/**
98-
* A factory function for creating new domainless OpenFeature clients.
94+
* A factory function for creating new domainless OpenFeature client.
9995
* Clients can contain their own state (e.g. logger, hook, context).
10096
* Multiple clients can be used to segment feature flag configuration.
10197
* If there is already a provider bound to this domain, this provider will be used.
10298
* Otherwise, the default provider is used until a provider is assigned to that domain.
103-
*
104-
* @param domain a identifier which logically binds clients with providers
99+
*
100+
* @param domain a identifier which logically binds clients with providers
105101
* @param version a version identifier
106102
* @return a new client instance
107103
*/
108104
public Client getClient(String domain, String version) {
109-
return new OpenFeatureClient(this,
105+
return new OpenFeatureClient(
106+
this,
110107
domain,
111-
version);
108+
version
109+
);
112110
}
113111

114112
/**
@@ -193,8 +191,8 @@ public void setProvider(FeatureProvider provider) {
193191
/**
194192
* Add a provider for a domain.
195193
*
196-
* @param domain The domain to bind the provider to.
197-
* @param provider The provider to set.
194+
* @param domain The domain to bind the provider to.
195+
* @param provider The provider to set.
198196
*/
199197
public void setProvider(String domain, FeatureProvider provider) {
200198
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
@@ -226,8 +224,8 @@ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError
226224
/**
227225
* Add a provider for a domain and wait for initialization to finish.
228226
*
229-
* @param domain The domain to bind the provider to.
230-
* @param provider The provider to set.
227+
* @param domain The domain to bind the provider to.
228+
* @param provider The provider to set.
231229
*/
232230
public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError {
233231
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
@@ -300,6 +298,7 @@ public void addHooks(Hook... hooks) {
300298

301299
/**
302300
* Fetch the hooks associated to this client.
301+
*
303302
* @return A list of {@link Hook}s.
304303
*/
305304
public List<Hook> getHooks() {
@@ -394,17 +393,21 @@ void removeHandler(String domain, ProviderEvent event, Consumer<EventDetails> ha
394393
void addHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
395394
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
396395
// if the provider is in the state associated with event, run immediately
397-
if (Optional.ofNullable(this.providerRepository.getProvider(domain).getState())
396+
if (Optional.ofNullable(this.providerRepository.getProviderState(domain))
398397
.orElse(ProviderState.READY).matchesEvent(event)) {
399398
eventSupport.runHandler(handler, EventDetails.builder().domain(domain).build());
400399
}
401400
eventSupport.addClientHandler(domain, event, handler);
402401
}
403402
}
404403

404+
FeatureProviderStateManager getFeatureProviderStateManager(String domain) {
405+
return providerRepository.getFeatureProviderStateManager(domain);
406+
}
407+
405408
/**
406409
* Runs the handlers associated with a particular provider.
407-
*
410+
*
408411
* @param provider the provider from where this event originated
409412
* @param event the event type
410413
* @param details the event details

0 commit comments

Comments
 (0)