Skip to content

Commit ebff404

Browse files
committed
feat: events
Signed-off-by: Todd Baert <[email protected]>
1 parent 89cedb9 commit ebff404

22 files changed

+1034
-200
lines changed

pom.xml

+3
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
<goals>
191191
<goal>cpu-count</goal>
192192
</goals>
193+
<configuration>
194+
<factor>0.25</factor>
195+
</configuration>
193196
</execution>
194197
</executions>
195198
</plugin>

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/**
66
* Interface used to resolve flags of varying types.
77
*/
8-
public interface Client extends Features {
8+
public interface Client extends Features, EventHandling<Client> {
99
Metadata getMetadata();
1010

1111
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package dev.openfeature.sdk;
2+
3+
import lombok.Data;
4+
import lombok.experimental.SuperBuilder;
5+
6+
/**
7+
* Interface for attaching event handlers.
8+
*/
9+
@Data @SuperBuilder(toBuilder = true)
10+
public class EventDetails extends ProviderEventDetails {
11+
private String clientName;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
import java.util.concurrent.ExecutorService;
9+
import java.util.concurrent.Executors;
10+
import java.util.function.Consumer;
11+
12+
import javax.annotation.Nullable;
13+
14+
import dev.openfeature.sdk.internal.AutoCloseableLock;
15+
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
16+
import lombok.extern.slf4j.Slf4j;
17+
18+
/**
19+
* Event emitter construct to be used by providers.
20+
*/
21+
@Slf4j
22+
public class EventEmitter {
23+
24+
private static final ExecutorService taskExecutor = Executors.newCachedThreadPool();
25+
private final Map<ProviderEvent, List<Consumer<EventDetails>>> handlerMap;
26+
private AutoCloseableReentrantReadWriteLock handlersLock = new AutoCloseableReentrantReadWriteLock();
27+
28+
/**
29+
* Construct a new EventEmitter.
30+
*/
31+
public EventEmitter() {
32+
handlerMap = new ConcurrentHashMap<ProviderEvent, List<Consumer<EventDetails>>>();
33+
handlerMap.put(ProviderEvent.PROVIDER_READY, new ArrayList<>());
34+
handlerMap.put(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, new ArrayList<>());
35+
handlerMap.put(ProviderEvent.PROVIDER_ERROR, new ArrayList<>());
36+
handlerMap.put(ProviderEvent.PROVIDER_STALE, new ArrayList<>());
37+
}
38+
39+
/**
40+
* Emit an event.
41+
*
42+
* @param event Event type to emit.
43+
* @param details Event details.
44+
*/
45+
public void emit(ProviderEvent event, ProviderEventDetails details) {
46+
try (AutoCloseableLock __ = handlersLock.readLockAutoCloseable()) {
47+
EventDetails eventDetails = EventDetails.builder()
48+
.flagMetadata(details.getFlagMetadata())
49+
.flagsChanged(details.getFlagsChanged())
50+
.message(details.getMessage())
51+
.build();
52+
53+
// we may be forwarding this event, preserve the name if so.
54+
if (EventDetails.class.isInstance(details)) {
55+
eventDetails.setClientName(((EventDetails) details).getClientName());
56+
}
57+
58+
this.handlerMap.get(event).stream().forEach(handler -> {
59+
runHander(handler, eventDetails);
60+
});
61+
}
62+
}
63+
64+
void runHander(Consumer<EventDetails> handler, EventDetails eventDetails) {
65+
taskExecutor.submit(() -> {
66+
try {
67+
handler.accept(eventDetails);
68+
} catch (Exception e) {
69+
log.error("Exception in event handler {}", handler, e);
70+
}
71+
});
72+
}
73+
74+
void addHandler(ProviderEvent event, Consumer<EventDetails> handler) {
75+
try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) {
76+
this.handlerMap.get(event).add(handler);
77+
}
78+
}
79+
80+
void removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
81+
try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) {
82+
this.handlerMap.get(event).remove(handler);
83+
}
84+
}
85+
86+
void removeAllHandlers() {
87+
try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) {
88+
this.handlerMap.keySet().stream()
89+
.forEach(type -> handlerMap.get(type).clear());
90+
}
91+
}
92+
93+
/**
94+
* Propagates all events from the originatingEmitter to this one.
95+
*
96+
* @param originatingEmitter The emitter to forward events from.
97+
* @param clientName The client name that will be added to the events.
98+
*/
99+
void forwardEvents(EventEmitter originatingEmitter, @Nullable String clientName) {
100+
Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
101+
originatingEmitter.addHandler(eventType, details -> {
102+
// set the client name when we proxy the events through.
103+
details.setClientName(clientName);
104+
this.emit(eventType, details);
105+
});
106+
});
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.function.Consumer;
4+
5+
/**
6+
* Interface for attaching event handlers.
7+
*/
8+
public interface EventHandling<T> {
9+
10+
T onProviderReady(Consumer<EventDetails> handler);
11+
12+
T onProviderConfigurationChanged(Consumer<EventDetails> handler);
13+
14+
T onProviderError(Consumer<EventDetails> handler);
15+
16+
T onProviderStale(Consumer<EventDetails> handler);
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* Interface for attaching event handlers.
5+
*
6+
* @see SimpleEventProvider for a basic implementation.
7+
*/
8+
public interface EventProvider {
9+
10+
/**
11+
* Return the EventEmitter interface for this provider.
12+
* The same instance should be returned from this method at each invocation.
13+
*
14+
* @return the EventEmitter instance for this EventProvider.
15+
*/
16+
EventEmitter getEventEmitter();
17+
18+
static boolean isEventProvider(FeatureProvider provider) {
19+
return EventProvider.class.isInstance(provider);
20+
}
21+
}

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

+25-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import java.util.List;
55

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

2627
/**
27-
* This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
28-
* if they have special initialization needed prior being called for flag evaluation.
28+
* This method is called before a provider is used to evaluate flags. Providers
29+
* can overwrite this method,
30+
* if they have special initialization needed prior being called for flag
31+
* evaluation.
2932
* <p>
30-
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
33+
* It is ok, if the method is expensive as it is executed in the background. All
34+
* runtime exceptions will be
3135
* caught and logged.
3236
* </p>
3337
*/
34-
default void initialize() {
38+
default void initialize(EvaluationContext evaluationContext) throws Exception {
3539
// Intentionally left blank
3640
}
3741

3842
/**
39-
* This method is called when a new provider is about to be used to evaluate flags, or the SDK is shut down.
40-
* Providers can overwrite this method, if they have special shutdown actions needed.
43+
* This method is called when a new provider is about to be used to evaluate
44+
* flags, or the SDK is shut down.
45+
* Providers can overwrite this method, if they have special shutdown actions
46+
* needed.
4147
* <p>
42-
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
48+
* It is ok, if the method is expensive as it is executed in the background. All
49+
* runtime exceptions will be
4350
* caught and logged.
4451
* </p>
4552
*/
4653
default void shutdown() {
4754
// Intentionally left blank
4855
}
4956

57+
/**
58+
* Returns a representation of the current readiness of the provider.
59+
* Providers which do not implement this method are assumed to be ready immediately.
60+
*
61+
* @return ProviderState
62+
*/
63+
default ProviderState getState() {
64+
return ProviderState.READY;
65+
}
66+
5067
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ public class NoOpProvider implements FeatureProvider {
1010
@Getter
1111
private final String name = "No-op Provider";
1212

13+
// The Noop provider is ALWAYS NOT_READY, otherwise READY handlers would run immediately when attached.
14+
@Override
15+
public ProviderState getState() {
16+
return ProviderState.NOT_READY;
17+
}
18+
1319
@Override
1420
public Metadata getMetadata() {
1521
return new Metadata() {

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

+59-11
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
package dev.openfeature.sdk;
22

3-
import dev.openfeature.sdk.internal.AutoCloseableLock;
4-
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
5-
import lombok.extern.slf4j.Slf4j;
6-
7-
import javax.annotation.Nullable;
83
import java.util.ArrayList;
94
import java.util.Arrays;
105
import java.util.List;
6+
import java.util.function.Consumer;
7+
8+
import javax.annotation.Nullable;
9+
10+
import dev.openfeature.sdk.internal.AutoCloseableLock;
11+
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
12+
import lombok.extern.slf4j.Slf4j;
1113

1214
/**
13-
* A global singleton which holds base configuration for the OpenFeature library.
15+
* A global singleton which holds base configuration for the OpenFeature
16+
* library.
1417
* Configuration here will be shared across all {@link Client}s.
1518
*/
1619
@Slf4j
17-
public class OpenFeatureAPI {
20+
public class OpenFeatureAPI implements EventHandling<OpenFeatureAPI> {
1821
// package-private multi-read/single-write lock
1922
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
2023
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
21-
24+
private EvaluationContext evaluationContext;
2225
private final List<Hook> apiHooks;
23-
2426
private ProviderRepository providerRepository = new ProviderRepository();
25-
private EvaluationContext evaluationContext;
27+
final EventEmitter emitter = new EventEmitter();
2628

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

54+
/**
55+
* {@inheritDoc}
56+
*/
5257
public Client getClient() {
5358
return getClient(null, null);
5459
}
5560

61+
/**
62+
* {@inheritDoc}
63+
*/
5664
public Client getClient(@Nullable String name) {
5765
return getClient(name, null);
5866
}
5967

68+
/**
69+
* {@inheritDoc}
70+
*/
6071
public Client getClient(@Nullable String name, @Nullable String version) {
61-
return new OpenFeatureClient(this, name, version);
72+
return new OpenFeatureClient(this,
73+
() -> this.providerRepository.getProvider(name).getState(),
74+
this.providerRepository.getAndCacheEmitter(name),
75+
name,
76+
version);
6277
}
6378

6479
/**
@@ -83,6 +98,7 @@ public EvaluationContext getEvaluationContext() {
8398
* Set the default provider.
8499
*/
85100
public void setProvider(FeatureProvider provider) {
101+
propagateEventsIfSupported(provider, null);
86102
providerRepository.setProvider(provider);
87103
}
88104

@@ -93,6 +109,7 @@ public void setProvider(FeatureProvider provider) {
93109
* @param provider The provider to set.
94110
*/
95111
public void setProvider(String clientName, FeatureProvider provider) {
112+
propagateEventsIfSupported(provider, clientName);
96113
providerRepository.setProvider(clientName, provider);
97114
}
98115

@@ -144,6 +161,37 @@ public void shutdown() {
144161
providerRepository.shutdown();
145162
}
146163

164+
@Override
165+
public OpenFeatureAPI onProviderReady(Consumer<EventDetails> handler) {
166+
return this.on(ProviderEvent.PROVIDER_READY, handler);
167+
}
168+
169+
@Override
170+
public OpenFeatureAPI onProviderConfigurationChanged(Consumer<EventDetails> handler) {
171+
return this.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler);
172+
}
173+
174+
@Override
175+
public OpenFeatureAPI onProviderError(Consumer<EventDetails> handler) {
176+
return this.on(ProviderEvent.PROVIDER_ERROR, handler);
177+
}
178+
179+
@Override
180+
public OpenFeatureAPI onProviderStale(Consumer<EventDetails> handler) {
181+
return this.on(ProviderEvent.PROVIDER_STALE, handler);
182+
}
183+
184+
private OpenFeatureAPI on(ProviderEvent event, Consumer<EventDetails> consumer) {
185+
this.emitter.addHandler(event, consumer);
186+
return this;
187+
}
188+
189+
private void propagateEventsIfSupported(FeatureProvider provider, @Nullable String clientName) {
190+
if (EventProvider.isEventProvider(provider)) {
191+
this.emitter.forwardEvents(((EventProvider) provider).getEventEmitter(), clientName);
192+
}
193+
}
194+
147195
/**
148196
* This method is only here for testing as otherwise all tests after the API shutdown test would fail.
149197
*/

0 commit comments

Comments
 (0)