Skip to content

Commit

Permalink
Move StackedEmitCallsProvider to testUtils
Browse files Browse the repository at this point in the history
Signed-off-by: Philipp Fehre <[email protected]>
  • Loading branch information
Philipp Fehre committed Feb 4, 2025
1 parent 12fac55 commit b9f3afe
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 101 deletions.
5 changes: 2 additions & 3 deletions src/main/java/dev/openfeature/sdk/EventProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
@Slf4j
public abstract class EventProvider implements FeatureProvider {
private EventProviderListener eventProviderListener;
private static final int SHUTDOWN_TIMEOUT_SECONDS = 3;
private static final ExecutorService emitterExecutor = Executors.newCachedThreadPool(runnable -> {
private final ExecutorService emitterExecutor = Executors.newCachedThreadPool(runnable -> {
final Thread thread = new Thread(runnable);
thread.setDaemon(true);
return thread;
Expand Down Expand Up @@ -65,7 +64,7 @@ void detach() {
public void shutdown() {
emitterExecutor.shutdown();
try {
if (!emitterExecutor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
if (!emitterExecutor.awaitTermination(EventSupport.SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
log.warn("Emitter executor did not terminate before the timeout period had elapsed");
emitterExecutor.shutdownNow();
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dev/openfeature/sdk/EventSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
@Slf4j
class EventSupport {

public static final int SHUTDOWN_TIMEOUT_SECONDS = 3;

// 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 int SHUTDOWN_TIMEOUT_SECONDS = 3;
private final Map<String, HandlerStore> handlerStores = new ConcurrentHashMap<>();
private final HandlerStore globalHandlerStore = new HandlerStore();
private final ExecutorService taskExecutor = Executors.newCachedThreadPool(runnable -> {
Expand Down
105 changes: 8 additions & 97 deletions src/test/java/dev/openfeature/sdk/EventProviderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import static org.mockito.Mockito.verify;

import dev.openfeature.sdk.internal.TriConsumer;
import java.util.function.Consumer;
import dev.openfeature.sdk.testutils.TestStackedEmitCallsProvider;
import io.cucumber.java.AfterAll;
import lombok.SneakyThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -26,6 +27,11 @@ void setup() {
eventProvider.initialize(null);
}

@AfterAll
public static void resetDefaultProvider() {
OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider());
}

@Test
@DisplayName("should run attached onEmit with emitters")
void emitsEventsWhenAttached() {
Expand Down Expand Up @@ -85,105 +91,10 @@ void doesNotThrowWhenOnEmitSame() {
@Timeout(value = 2, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
@DisplayName("should not deadlock on emit called during emit")
void doesNotDeadlockOnEmitStackedCalls() {
StackedEmitCallsProvider provider = new StackedEmitCallsProvider();
TestStackedEmitCallsProvider provider = new TestStackedEmitCallsProvider();
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
}

static class StackedEmitCallsProvider extends EventProvider {
private final NestedBlockingEmitter nestedBlockingEmitter = new NestedBlockingEmitter(this::onProviderEvent);

@Override
public Metadata getMetadata() {
return () -> getClass().getSimpleName();
}

@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
synchronized (nestedBlockingEmitter) {
nestedBlockingEmitter.init();
while (!nestedBlockingEmitter.isReady()) {
try {
nestedBlockingEmitter.wait();
} catch (InterruptedException e) {
}
}
}
}

private void onProviderEvent(ProviderEvent providerEvent) {
synchronized (nestedBlockingEmitter) {
if (providerEvent == ProviderEvent.PROVIDER_READY) {
nestedBlockingEmitter.setReady();
/*
* This line deadlocked in the original implementation without the emitterExecutor see
* https://github.com/open-feature/java-sdk/issues/1299
*/
emitProviderReady(ProviderEventDetails.builder().build());
}
}
}

@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(
String key, Boolean defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'");
}

@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'");
}

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(
String key, Integer defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'");
}

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'");
}

@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'");
}
}

static class NestedBlockingEmitter {

private final Consumer<ProviderEvent> emitProviderEvent;
private volatile boolean isReady;

public NestedBlockingEmitter(Consumer<ProviderEvent> emitProviderEvent) {
this.emitProviderEvent = emitProviderEvent;
}

public void init() {
// run init outside monitored thread
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

emitProviderEvent.accept(ProviderEvent.PROVIDER_READY);
})
.start();
}

public boolean isReady() {
return isReady;
}

public synchronized void setReady() {
isReady = true;
this.notifyAll();
}
}

static class TestEventProvider extends EventProvider {

private static final String NAME = "TestEventProvider";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package dev.openfeature.sdk.testutils;

import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.EventProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.ProviderEvent;
import dev.openfeature.sdk.ProviderEventDetails;
import dev.openfeature.sdk.Value;
import java.util.function.Consumer;

public class TestStackedEmitCallsProvider extends EventProvider {
private final NestedBlockingEmitter nestedBlockingEmitter = new NestedBlockingEmitter(this::onProviderEvent);

@Override
public Metadata getMetadata() {
return () -> getClass().getSimpleName();
}

@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
synchronized (nestedBlockingEmitter) {
nestedBlockingEmitter.init();
while (!nestedBlockingEmitter.isReady()) {
try {
nestedBlockingEmitter.wait();
} catch (InterruptedException e) {
}
}
}
}

private void onProviderEvent(ProviderEvent providerEvent) {
synchronized (nestedBlockingEmitter) {
if (providerEvent == ProviderEvent.PROVIDER_READY) {
nestedBlockingEmitter.setReady();
/*
* This line deadlocked in the original implementation without the emitterExecutor see
* https://github.com/open-feature/java-sdk/issues/1299
*/
emitProviderReady(ProviderEventDetails.builder().build());
}
}
}

@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'");
}

@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'");
}

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'");
}

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'");
}

@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'");
}

static class NestedBlockingEmitter {

private final Consumer<ProviderEvent> emitProviderEvent;
private volatile boolean isReady;

public NestedBlockingEmitter(Consumer<ProviderEvent> emitProviderEvent) {
this.emitProviderEvent = emitProviderEvent;
}

public void init() {
// run init outside monitored thread
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

emitProviderEvent.accept(ProviderEvent.PROVIDER_READY);
})
.start();
}

public boolean isReady() {
return isReady;
}

public synchronized void setReady() {
isReady = true;
this.notifyAll();
}
}
}

0 comments on commit b9f3afe

Please sign in to comment.