Skip to content

Commit

Permalink
feat: make provider interface "stateless"; SDK maintains provider sta…
Browse files Browse the repository at this point in the history
…te (#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]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent dd8ba81 commit 1b1e527
Show file tree
Hide file tree
Showing 28 changed files with 1,070 additions and 580 deletions.
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,6 @@ public class MyProvider implements FeatureProvider {
return () -> "My Provider";
}

@Override
public ProviderState getState() {
// optionally indicate your provider's state (assumed to be READY if not implemented)
}

@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
// start up your provider
Expand Down Expand Up @@ -368,11 +363,6 @@ class MyEventProvider extends EventProvider {
return () -> "My Event Provider";
}

@Override
public ProviderState getState() {
// indicate your provider's state (required for EventProviders)
}

@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
// emit events when flags are changed in a hypothetical REST API
Expand All @@ -391,6 +381,13 @@ class MyEventProvider extends EventProvider {
}
```

Providers no longer need to manage their own state, this is done by the SDK itself. If desired, the state of a provider
can be queried through the client that uses the provider.

```java
OpenFeatureAPI.getInstance().getClient().getProviderState();
```

> 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!
### Develop a hook
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/dev/openfeature/sdk/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ public interface Client extends Features, EventBus<Client> {
* @return A list of {@link Hook}s.
*/
List<Hook> getHooks();

/**
* Returns the current state of the associated provider.
* @return the provider state
*/
ProviderState getProviderState();
}
3 changes: 2 additions & 1 deletion src/main/java/dev/openfeature/sdk/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

@SuppressWarnings("checkstyle:MissingJavadocType")
public enum ErrorCode {
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL,
PROVIDER_FATAL
}
26 changes: 14 additions & 12 deletions src/main/java/dev/openfeature/sdk/EventProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dev.openfeature.sdk.internal.TriConsumer;


/**
* Abstract EventProvider. Providers must extend this class to support events.
* Emit events with {@link #emit(ProviderEvent, ProviderEventDetails)}. Please
Expand All @@ -15,22 +16,20 @@
* @see FeatureProvider
*/
public abstract class EventProvider implements FeatureProvider {
private EventProviderListener eventProviderListener;

/**
* {@inheritDoc}
*/
@Override
public abstract ProviderState getState();
void setEventProviderListener(EventProviderListener eventProviderListener) {
this.eventProviderListener = eventProviderListener;
}

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

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

/**
* Emit the specified {@link ProviderEvent}.
*
*
* @param event The event type
* @param details The details of the event
*/
public void emit(ProviderEvent event, ProviderEventDetails details) {
if (eventProviderListener != null) {
eventProviderListener.onEmit(event, details);
}
if (this.onEmit != null) {
this.onEmit.accept(this, event, details);
}
Expand All @@ -63,7 +65,7 @@ public void emit(ProviderEvent event, ProviderEventDetails details) {
/**
* Emit a {@link ProviderEvent#PROVIDER_READY} event.
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
*
*
* @param details The details of the event
*/
public void emitProviderReady(ProviderEventDetails details) {
Expand All @@ -74,7 +76,7 @@ public void emitProviderReady(ProviderEventDetails details) {
* Emit a
* {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED}
* event. Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
*
*
* @param details The details of the event
*/
public void emitProviderConfigurationChanged(ProviderEventDetails details) {
Expand All @@ -84,7 +86,7 @@ public void emitProviderConfigurationChanged(ProviderEventDetails details) {
/**
* Emit a {@link ProviderEvent#PROVIDER_STALE} event.
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
*
*
* @param details The details of the event
*/
public void emitProviderStale(ProviderEventDetails details) {
Expand All @@ -94,7 +96,7 @@ public void emitProviderStale(ProviderEventDetails details) {
/**
* Emit a {@link ProviderEvent#PROVIDER_ERROR} event.
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
*
*
* @param details The details of the event
*/
public void emitProviderError(ProviderEventDetails details) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventProviderListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.openfeature.sdk;

@FunctionalInterface
interface EventProviderListener {
void onEmit(ProviderEvent event, ProviderEventDetails details);
}
6 changes: 4 additions & 2 deletions src/main/java/dev/openfeature/sdk/FeatureProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ default void shutdown() {
* If the provider needs to be initialized, it should return {@link ProviderState#NOT_READY}.
* If the provider is in an error state, it should return {@link ProviderState#ERROR}.
* If the provider is functioning normally, it should return {@link ProviderState#READY}.
*
*
* <p><i>Providers which do not implement this method are assumed to be ready immediately.</i></p>
*
*
* @return ProviderState
* @deprecated The state is handled by the SDK internally. Query the state from the {@link Client} instead.
*/
@Deprecated
default ProviderState getState() {
return ProviderState.READY;
}
Expand Down
71 changes: 71 additions & 0 deletions src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.exceptions.OpenFeatureError;
import lombok.Getter;

import java.util.concurrent.atomic.AtomicBoolean;

class FeatureProviderStateManager implements EventProviderListener {
private final FeatureProvider delegate;
private final AtomicBoolean isInitialized = new AtomicBoolean();
@Getter
private ProviderState state = ProviderState.NOT_READY;

public FeatureProviderStateManager(FeatureProvider delegate) {
this.delegate = delegate;
if (delegate instanceof EventProvider) {
((EventProvider) delegate).setEventProviderListener(this);
}
}

public void initialize(EvaluationContext evaluationContext) throws Exception {
if (isInitialized.getAndSet(true)) {
return;
}
try {
delegate.initialize(evaluationContext);
state = ProviderState.READY;
} catch (OpenFeatureError openFeatureError) {
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
state = ProviderState.FATAL;
} else {
state = ProviderState.ERROR;
}
isInitialized.set(false);
throw openFeatureError;
} catch (Exception e) {
state = ProviderState.ERROR;
isInitialized.set(false);
throw e;
}
}

public void shutdown() {
delegate.shutdown();
state = ProviderState.NOT_READY;
isInitialized.set(false);
}

@Override
public void onEmit(ProviderEvent event, ProviderEventDetails details) {
if (ProviderEvent.PROVIDER_ERROR.equals(event)) {
if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) {
state = ProviderState.FATAL;
} else {
state = ProviderState.ERROR;
}
} else if (ProviderEvent.PROVIDER_STALE.equals(event)) {
state = ProviderState.STALE;
} else if (ProviderEvent.PROVIDER_READY.equals(event)) {
state = ProviderState.READY;
}
}

FeatureProvider getProvider() {
return delegate;
}

public boolean hasSameProvider(FeatureProvider featureProvider) {
return this.delegate.equals(featureProvider);
}
}
2 changes: 1 addition & 1 deletion src/main/java/dev/openfeature/sdk/NoOpProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double default

@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue,
EvaluationContext invocationContext) {
EvaluationContext invocationContext) {
return ProviderEvaluation.<Value>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
Expand Down
41 changes: 22 additions & 19 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.function.Consumer;

/**
Expand Down Expand Up @@ -69,7 +65,7 @@ public Metadata getProviderMetadata(String domain) {
}

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

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

/**
* A factory function for creating new domainless OpenFeature clients.
* A factory function for creating new domainless OpenFeature client.
* Clients can contain their own state (e.g. logger, hook, context).
* Multiple clients can be used to segment feature flag configuration.
* If there is already a provider bound to this domain, this provider will be used.
* Otherwise, the default provider is used until a provider is assigned to that domain.
*
* @param domain a identifier which logically binds clients with providers
*
* @param domain a identifier which logically binds clients with providers
* @param version a version identifier
* @return a new client instance
*/
public Client getClient(String domain, String version) {
return new OpenFeatureClient(this,
return new OpenFeatureClient(
this,
domain,
version);
version
);
}

/**
Expand Down Expand Up @@ -193,8 +191,8 @@ public void setProvider(FeatureProvider provider) {
/**
* Add a provider for a domain.
*
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProvider(String domain, FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
Expand Down Expand Up @@ -226,8 +224,8 @@ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError
/**
* Add a provider for a domain and wait for initialization to finish.
*
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
Expand Down Expand Up @@ -300,6 +298,7 @@ public void addHooks(Hook... hooks) {

/**
* Fetch the hooks associated to this client.
*
* @return A list of {@link Hook}s.
*/
public List<Hook> getHooks() {
Expand Down Expand Up @@ -394,17 +393,21 @@ void removeHandler(String domain, ProviderEvent event, Consumer<EventDetails> ha
void addHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
// if the provider is in the state associated with event, run immediately
if (Optional.ofNullable(this.providerRepository.getProvider(domain).getState())
if (Optional.ofNullable(this.providerRepository.getProviderState(domain))
.orElse(ProviderState.READY).matchesEvent(event)) {
eventSupport.runHandler(handler, EventDetails.builder().domain(domain).build());
}
eventSupport.addClientHandler(domain, event, handler);
}
}

FeatureProviderStateManager getFeatureProviderStateManager(String domain) {
return providerRepository.getFeatureProviderStateManager(domain);
}

/**
* Runs the handlers associated with a particular provider.
*
*
* @param provider the provider from where this event originated
* @param event the event type
* @param details the event details
Expand Down
Loading

0 comments on commit 1b1e527

Please sign in to comment.