Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: events #476

Merged
merged 20 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,21 +178,6 @@

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<phase>validate</phase>
<id>get-cpu-count</id>
<goals>
<goal>cpu-count</goal>
</goals>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
Expand Down Expand Up @@ -261,9 +246,6 @@
<argLine>
${surefireArgLine}
</argLine>
<!-- fork a new JVM to isolate test suites, especially important with singletons -->
<forkCount>${cpu.count}</forkCount>
<reuseForks>false</reuseForks>
Comment on lines -264 to -266
Copy link
Member Author

@toddbaert toddbaert Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As witnessed by @Kavindu-Dodan the test suite is actually 3x faster without any forking at all, possibly due to JVM startup time 🤷

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This observation can be explained with the surefire documentation [1]

The parameter reuseForks is used to define whether to terminate the spawned process after one test class and to create a new process for the next test in line (reuseForks=false), or whether to reuse the processes to execute the next tests (reuseForks=true).

forkCount helped with creating N parallel forks but, given that reuseForks was false, tests did not reuse them. This means, while tests ran in parallel forks, new tests require a new process, adding a delay between executions.

[1] - https://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html#forked-test-execution

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

I guessed it was something like that. It makes sense that starting up a bunch of JVMs would slow things down.

I did just try <forkCount>1C</forkCount> (which means 1 x number-of-cores), while allowing fork re-use, and the speed of the suite is unchanged from the default... so it doesn't slow things down but doesn't really help much.

<excludes>
<!-- tests to exclude -->
<exclude>${testExclusions}</exclude>
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
28 changes: 28 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.openfeature.sdk;

import edu.umd.cs.findbugs.annotations.Nullable;
import lombok.Data;
import lombok.experimental.SuperBuilder;

/**
* The details of a particular event.
*/
@Data @SuperBuilder(toBuilder = true)
public class EventDetails extends ProviderEventDetails {
private String clientName;

static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails) {
return EventDetails.fromProviderEventDetails(providerEventDetails, null);
}

static EventDetails fromProviderEventDetails(
ProviderEventDetails providerEventDetails,
@Nullable String clientName) {
return EventDetails.builder()
.clientName(clientName)
.flagsChanged(providerEventDetails.getFlagsChanged())
.eventMetadata(providerEventDetails.getEventMetadata())
.message(providerEventDetails.getMessage())
.build();
}
}
64 changes: 64 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventHandling.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package dev.openfeature.sdk;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the public interface for both the API and client events.


import java.util.function.Consumer;

/**
* Interface for attaching event handlers.
*/
public interface EventHandling<T> {
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
justinabrahms marked this conversation as resolved.
Show resolved Hide resolved

/**
* Add a handler for the {@link ProviderEvent#PROVIDER_READY} event.
* Shorthand for {@link #on(ProviderEvent, Consumer)}
*
* @param handler behavior to add with this event
* @return this
*/
T onProviderReady(Consumer<EventDetails> handler);

/**
* Add a handler for the {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED} event.
* Shorthand for {@link #on(ProviderEvent, Consumer)}
*
* @param handler behavior to add with this event
* @return this
*/
T onProviderConfigurationChanged(Consumer<EventDetails> handler);

/**
* Add a handler for the {@link ProviderEvent#PROVIDER_STALE} event.
* Shorthand for {@link #on(ProviderEvent, Consumer)}
*
* @param handler behavior to add with this event
* @return this
*/
T onProviderError(Consumer<EventDetails> handler);

/**
* Add a handler for the {@link ProviderEvent#PROVIDER_ERROR} event.
* Shorthand for {@link #on(ProviderEvent, Consumer)}
*
* @param handler behavior to add with this event
* @return this
*/
T onProviderStale(Consumer<EventDetails> handler);

/**
* Add a handler for the specified {@link ProviderEvent}.
*
* @param event event type
* @param handler behavior to add with this event
* @return this
*/
T on(ProviderEvent event, Consumer<EventDetails> handler);

/**
* Remove the previously attached handler by reference.
* If the handler doesn't exists, no-op.
*
* @param event event type
* @param handler to be removed
* @return this
*/
T removeHandler(ProviderEvent event, Consumer<EventDetails> handler);
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
}
83 changes: 83 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.internal.TriConsumer;

/**
* Abstract EventProvider. Providers must extend this class to support events.
* Emit events with {@link #emit(ProviderEvent, ProviderEventDetails)}. Please
* note that the SDK will automatically emit
* {@link ProviderEvent#PROVIDER_READY } or
* {@link ProviderEvent#PROVIDER_ERROR } accordingly when
* {@link FeatureProvider#initialize(EvaluationContext)} completes successfully
* or with error, so these events need not be emitted manually during
* initialization.
*
* @see FeatureProvider
*/
public abstract class EventProvider implements FeatureProvider {
Copy link
Member Author

@toddbaert toddbaert Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the class application authors must extend to use events. See javadoc above.


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

void detach() {
this.onEmit = null;
}

void attach(TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit) {
if (this.onEmit == null) {
justinabrahms marked this conversation as resolved.
Show resolved Hide resolved
this.onEmit = onEmit;
}
}

/**
* 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 (this.onEmit != null) {
this.onEmit.accept(this, event, 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) {
emit(ProviderEvent.PROVIDER_READY, 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) {
emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, 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) {
emit(ProviderEvent.PROVIDER_STALE, 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) {
emit(ProviderEvent.PROVIDER_ERROR, details);
}
}
115 changes: 115 additions & 0 deletions src/main/java/dev/openfeature/sdk/EventSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package dev.openfeature.sdk;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

import edu.umd.cs.findbugs.annotations.Nullable;
import lombok.extern.slf4j.Slf4j;

/**
* Util class for storing and running handlers.
*/
@Slf4j
class EventSupport {

// we use a v4 uuid as a "placeholder" for anonymous clients, since
// ConcurrentHashMap doesn't support nulls
private static final String defaultClientUuid = UUID.randomUUID().toString();
private static final ExecutorService taskExecutor = Executors.newCachedThreadPool();
private final Map<String, HandlerStore> handlerStores = new ConcurrentHashMap<>();
private final HandlerStore globalHandlerStore = new HandlerStore();

public void runClientHandlers(@Nullable String clientName, ProviderEvent event, EventDetails eventDetails) {
justinabrahms marked this conversation as resolved.
Show resolved Hide resolved
clientName = Optional.ofNullable(clientName)
.orElse(defaultClientUuid);

// run handlers if they exist
Optional.ofNullable(handlerStores.get(clientName))
.filter(store -> Optional.of(store).isPresent())
.map(store -> store.handlerMap.get(event))
.ifPresent(handlers -> handlers
.forEach(handler -> runHandler(handler, eventDetails)));
}

public void runGlobalHandlers(ProviderEvent event, EventDetails eventDetails) {
globalHandlerStore.handlerMap.get(event)
.forEach(handler -> {
runHandler(handler, eventDetails);
});
}

public void addClientHandler(@Nullable String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
final String name = Optional.ofNullable(clientName)
.orElse(defaultClientUuid);

// lazily create and cache a HandlerStore if it doesn't exist
HandlerStore store = Optional.ofNullable(this.handlerStores.get(name))
.orElseGet(() -> {
HandlerStore newStore = new HandlerStore();
this.handlerStores.put(name, newStore);
return newStore;
});
store.addHandler(event, handler);
}

public void removeClientHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
clientName = Optional.ofNullable(clientName)
.orElse(defaultClientUuid);
this.handlerStores.get(clientName).removeHandler(event, handler);
}

public void addGlobalHandler(ProviderEvent event, Consumer<EventDetails> handler) {
this.globalHandlerStore.addHandler(event, handler);
}

public void removeGlobalHandler(ProviderEvent event, Consumer<EventDetails> handler) {
this.globalHandlerStore.removeHandler(event, handler);
}

public Set<String> getAllClientNames() {
return this.handlerStores.keySet();
}

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

public void shutdown() {
taskExecutor.shutdown();
}

static class HandlerStore {

private final Map<ProviderEvent, List<Consumer<EventDetails>>> handlerMap;

{
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<>());
}

void addHandler(ProviderEvent event, Consumer<EventDetails> handler) {
handlerMap.get(event).add(handler);
}

void removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
handlerMap.get(event).remove(handler);
}
}
}
34 changes: 26 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,9 @@
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. If you want to support realtime events with your provider, you
* should extend {@link EventProvider}
*/
public interface FeatureProvider {
Metadata getMetadata();
Expand All @@ -24,27 +26,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;
}

}
Loading