Skip to content

Commit

Permalink
feat: events
Browse files Browse the repository at this point in the history
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
  • Loading branch information
toddbaert committed Jun 14, 2023
1 parent 89cedb9 commit bc6622a
Show file tree
Hide file tree
Showing 22 changed files with 1,026 additions and 200 deletions.
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@
<goals>
<goal>cpu-count</goal>
</goals>
<configuration>
<factor>0.25</factor>
</configuration>
</execution>
</executions>
</plugin>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/dev/openfeature/sdk/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/**
* Interface used to resolve flags of varying types.
*/
public interface Client extends Features {
public interface Client extends Features, EventHandling<Client> {
Metadata getMetadata();

/**
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.openfeature.sdk;

import lombok.Data;
import lombok.experimental.SuperBuilder;

/**
* Interface for attaching event handlers.
*/
@Data @SuperBuilder(toBuilder = true)
public class EventDetails extends ProviderEventDetails {
private String clientName;
}
108 changes: 108 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventEmitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package dev.openfeature.sdk;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

import javax.annotation.Nullable;

import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;

/**
* Event emitter construct to be used by providers.
*/
@Slf4j
public class EventEmitter {

private static final ExecutorService taskExecutor = Executors.newCachedThreadPool();
private final Map<ProviderEvent, List<Consumer<EventDetails>>> handlerMap;
private AutoCloseableReentrantReadWriteLock handlersLock = new AutoCloseableReentrantReadWriteLock();

/**
* Construct a new EventEmitter.
*/
public EventEmitter() {
handlerMap = new ConcurrentHashMap<ProviderEvent, List<Consumer<EventDetails>>>();
handlerMap.put(ProviderEvent.PROVIDER_READY, new ArrayList<>());
handlerMap.put(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, new ArrayList<>());
handlerMap.put(ProviderEvent.PROVIDER_ERROR, new ArrayList<>());
handlerMap.put(ProviderEvent.PROVIDER_STALE, new ArrayList<>());
}

/**
* Emit an event.
*
* @param event Event type to emit.
* @param details Event details.
*/
public void emit(ProviderEvent event, ProviderEventDetails details) {
try (AutoCloseableLock __ = handlersLock.readLockAutoCloseable()) {
EventDetails eventDetails = EventDetails.builder()
.flagMetadata(details.getFlagMetadata())
.flagsChanged(details.getFlagsChanged())
.message(details.getMessage())
.build();

// we may be forwarding this event, preserve the name if so.
if (EventDetails.class.isInstance(details)) {
eventDetails.setClientName(((EventDetails) details).getClientName());
}

this.handlerMap.get(event).stream().forEach(handler -> {
runHander(handler, eventDetails);
});
}
}

void runHander(Consumer<EventDetails> handler, EventDetails eventDetails) {
taskExecutor.submit(() -> {
try {
handler.accept(eventDetails);
} catch (Exception e) {
log.error("Exception in event handler {}", handler, e);
}
});
}

void addHandler(ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) {
this.handlerMap.get(event).add(handler);
}
}

void removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) {
this.handlerMap.get(event).remove(handler);
}
}

void removeAllHandlers() {
try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) {
this.handlerMap.keySet().stream()
.forEach(type -> handlerMap.get(type).clear());
}
}

/**
* Propagates all events from the originatingEmitter to this one.
*
* @param originatingEmitter The emitter to forward events from.
* @param clientName The client name that will be added to the events.
*/
void forwardEvents(EventEmitter originatingEmitter, @Nullable String clientName) {
Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
originatingEmitter.addHandler(eventType, details -> {
// set the client name when we proxy the events through.
details.setClientName(clientName);
this.emit(eventType, details);
});
});
}
}
17 changes: 17 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventHandling.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.openfeature.sdk;

import java.util.function.Consumer;

/**
* Interface for attaching event handlers.
*/
public interface EventHandling<T> {

T onProviderReady(Consumer<EventDetails> handler);

T onProviderConfigurationChanged(Consumer<EventDetails> handler);

T onProviderError(Consumer<EventDetails> handler);

T onProviderStale(Consumer<EventDetails> handler);
}
21 changes: 21 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.openfeature.sdk;

/**
* Interface for attaching event handlers.
*
* @see SimpleEventProvider for a basic implementation.
*/
public interface EventProvider {

/**
* Return the EventEmitter interface for this provider.
* The same instance should be returned from this method at each invocation.
*
* @return the EventEmitter instance for this EventProvider.
*/
EventEmitter getEventEmitter();

static boolean isEventProvider(FeatureProvider provider) {
return EventProvider.class.isInstance(provider);
}
}
33 changes: 25 additions & 8 deletions src/main/java/dev/openfeature/sdk/FeatureProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import java.util.List;

/**
* The interface implemented by upstream flag providers to resolve flags for their service.
* The interface implemented by upstream flag providers to resolve flags for
* their service.
*/
public interface FeatureProvider {
Metadata getMetadata();
Expand All @@ -24,27 +25,43 @@ default List<Hook> getProviderHooks() {
ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx);

/**
* This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
* if they have special initialization needed prior being called for flag evaluation.
* This method is called before a provider is used to evaluate flags. Providers
* can overwrite this method,
* if they have special initialization needed prior being called for flag
* evaluation.
* <p>
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
* It is ok, if the method is expensive as it is executed in the background. All
* runtime exceptions will be
* caught and logged.
* </p>
*/
default void initialize() {
default void initialize(EvaluationContext evaluationContext) throws Exception {
// Intentionally left blank
}

/**
* This method is called when a new provider is about to be used to evaluate flags, or the SDK is shut down.
* Providers can overwrite this method, if they have special shutdown actions needed.
* This method is called when a new provider is about to be used to evaluate
* flags, or the SDK is shut down.
* Providers can overwrite this method, if they have special shutdown actions
* needed.
* <p>
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
* It is ok, if the method is expensive as it is executed in the background. All
* runtime exceptions will be
* caught and logged.
* </p>
*/
default void shutdown() {
// Intentionally left blank
}

/**
* Returns a representation of the current readiness of the provider.
* Providers which do not implement this method are assumed to be ready immediately.
*
* @return ProviderState
*/
default ProviderState getState() {
return ProviderState.READY;
}

}
6 changes: 6 additions & 0 deletions src/main/java/dev/openfeature/sdk/NoOpProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ public class NoOpProvider implements FeatureProvider {
@Getter
private final String name = "No-op Provider";

// The Noop provider is ALWAYS NOT_READY, otherwise READY handlers would run immediately when attached.
@Override
public ProviderState getState() {
return ProviderState.NOT_READY;
}

@Override
public Metadata getMetadata() {
return new Metadata() {
Expand Down
70 changes: 59 additions & 11 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import javax.annotation.Nullable;

import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;

/**
* A global singleton which holds base configuration for the OpenFeature library.
* A global singleton which holds base configuration for the OpenFeature
* library.
* Configuration here will be shared across all {@link Client}s.
*/
@Slf4j
public class OpenFeatureAPI {
public class OpenFeatureAPI implements EventHandling<OpenFeatureAPI> {
// package-private multi-read/single-write lock
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();

private EvaluationContext evaluationContext;
private final List<Hook> apiHooks;

private ProviderRepository providerRepository = new ProviderRepository();
private EvaluationContext evaluationContext;
final EventEmitter emitter = new EventEmitter();

protected OpenFeatureAPI() {
this.apiHooks = new ArrayList<>();
Expand All @@ -49,16 +51,29 @@ public Metadata getProviderMetadata(String clientName) {
return getProvider(clientName).getMetadata();
}

/**
* {@inheritDoc}
*/
public Client getClient() {
return getClient(null, null);
}

/**
* {@inheritDoc}
*/
public Client getClient(@Nullable String name) {
return getClient(name, null);
}

/**
* {@inheritDoc}
*/
public Client getClient(@Nullable String name, @Nullable String version) {
return new OpenFeatureClient(this, name, version);
return new OpenFeatureClient(this,
() -> this.providerRepository.getProvider(name).getState(),
this.providerRepository.getAndCacheEmitter(name),
name,
version);
}

/**
Expand All @@ -83,6 +98,7 @@ public EvaluationContext getEvaluationContext() {
* Set the default provider.
*/
public void setProvider(FeatureProvider provider) {
propagateEventsIfSupported(provider, null);
providerRepository.setProvider(provider);
}

Expand All @@ -93,6 +109,7 @@ public void setProvider(FeatureProvider provider) {
* @param provider The provider to set.
*/
public void setProvider(String clientName, FeatureProvider provider) {
propagateEventsIfSupported(provider, clientName);
providerRepository.setProvider(clientName, provider);
}

Expand Down Expand Up @@ -144,6 +161,37 @@ public void shutdown() {
providerRepository.shutdown();
}

@Override
public OpenFeatureAPI onProviderReady(Consumer<EventDetails> handler) {
return this.on(ProviderEvent.PROVIDER_READY, handler);
}

@Override
public OpenFeatureAPI onProviderConfigurationChanged(Consumer<EventDetails> handler) {
return this.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler);
}

@Override
public OpenFeatureAPI onProviderError(Consumer<EventDetails> handler) {
return this.on(ProviderEvent.PROVIDER_ERROR, handler);
}

@Override
public OpenFeatureAPI onProviderStale(Consumer<EventDetails> handler) {
return this.on(ProviderEvent.PROVIDER_STALE, handler);
}

private OpenFeatureAPI on(ProviderEvent event, Consumer<EventDetails> consumer) {
this.emitter.addHandler(event, consumer);
return this;
}

private void propagateEventsIfSupported(FeatureProvider provider, @Nullable String clientName) {
if (EventProvider.isEventProvider(provider)) {
this.emitter.forwardEvents(((EventProvider) provider).getEventEmitter(), clientName);
}
}

/**
* This method is only here for testing as otherwise all tests after the API shutdown test would fail.
*/
Expand Down
Loading

0 comments on commit bc6622a

Please sign in to comment.