Skip to content

Commit 20ba91c

Browse files
author
Philipp Fehre
committed
Move event emitting off the main thread to avoid deadlocks
When stacking event emitting inside an EventProvider, when using sychronization the EventProvider can deadlock, to avoid this move the event emitting of the main thread. Signed-off-by: Philipp Fehre <[email protected]>
1 parent 208411e commit 20ba91c

File tree

2 files changed

+141
-2
lines changed

2 files changed

+141
-2
lines changed

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package dev.openfeature.sdk;
22

33
import dev.openfeature.sdk.internal.TriConsumer;
4+
import java.util.concurrent.ExecutorService;
5+
import java.util.concurrent.Executors;
6+
import java.util.concurrent.TimeUnit;
7+
import lombok.extern.slf4j.Slf4j;
48

59
/**
610
* Abstract EventProvider. Providers must extend this class to support events.
@@ -14,8 +18,15 @@
1418
*
1519
* @see FeatureProvider
1620
*/
21+
@Slf4j
1722
public abstract class EventProvider implements FeatureProvider {
1823
private EventProviderListener eventProviderListener;
24+
private static final int SHUTDOWN_TIMEOUT_SECONDS = 3;
25+
private final ExecutorService emitterExecutor = Executors.newCachedThreadPool(runnable -> {
26+
final Thread thread = new Thread(runnable);
27+
thread.setDaemon(true);
28+
return thread;
29+
});
1930

2031
void setEventProviderListener(EventProviderListener eventProviderListener) {
2132
this.eventProviderListener = eventProviderListener;
@@ -46,6 +57,24 @@ void detach() {
4657
this.onEmit = null;
4758
}
4859

60+
/**
61+
* Stop the event emitter executor and block until either termination has completed
62+
* or timeout period has elapsed.
63+
*/
64+
@Override
65+
public void shutdown() {
66+
emitterExecutor.shutdown();
67+
try {
68+
if (!emitterExecutor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
69+
log.warn("Emitter executor did not terminate before the timeout period had elapsed");
70+
emitterExecutor.shutdownNow();
71+
}
72+
} catch (InterruptedException e) {
73+
emitterExecutor.shutdownNow();
74+
Thread.currentThread().interrupt();
75+
}
76+
}
77+
4978
/**
5079
* Emit the specified {@link ProviderEvent}.
5180
*
@@ -57,7 +86,7 @@ public void emit(ProviderEvent event, ProviderEventDetails details) {
5786
eventProviderListener.onEmit(event, details);
5887
}
5988
if (this.onEmit != null) {
60-
this.onEmit.accept(this, event, details);
89+
emitterExecutor.submit(() -> this.onEmit.accept(this, event, details));
6190
}
6291
}
6392

@@ -68,6 +97,7 @@ public void emit(ProviderEvent event, ProviderEventDetails details) {
6897
* @param details The details of the event
6998
*/
7099
public void emitProviderReady(ProviderEventDetails details) {
100+
System.out.println("Details: " + details);
71101
emit(ProviderEvent.PROVIDER_READY, details);
72102
}
73103

src/test/java/dev/openfeature/sdk/EventProviderTest.java

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
import static org.junit.jupiter.api.Assertions.assertThrows;
44
import static org.mockito.ArgumentMatchers.any;
5-
import static org.mockito.Mockito.*;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.never;
7+
import static org.mockito.Mockito.times;
8+
import static org.mockito.Mockito.verify;
69

710
import dev.openfeature.sdk.internal.TriConsumer;
11+
import java.util.function.Consumer;
812
import lombok.SneakyThrows;
913
import org.junit.jupiter.api.BeforeEach;
1014
import org.junit.jupiter.api.DisplayName;
1115
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.Timeout;
1217

1318
class EventProviderTest {
1419

@@ -75,6 +80,110 @@ void doesNotThrowWhenOnEmitSame() {
7580
eventProvider.attach(onEmit2); // should not throw, same instance. noop
7681
}
7782

83+
@Test
84+
@SneakyThrows
85+
@Timeout(value = 2, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
86+
@DisplayName("should not deadlock on emit called during emit")
87+
void doesNotDeadlockOnEmitStackedCalls() {
88+
StackedEmitCallsProvider provider = new StackedEmitCallsProvider();
89+
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
90+
}
91+
92+
static class StackedEmitCallsProvider extends EventProvider {
93+
private final NestedBlockingEmitter nestedBlockingEmitter = new NestedBlockingEmitter(this::onProviderEvent);
94+
95+
@Override
96+
public Metadata getMetadata() {
97+
return () -> getClass().getSimpleName();
98+
}
99+
100+
@Override
101+
public void initialize(EvaluationContext evaluationContext) throws Exception {
102+
synchronized (nestedBlockingEmitter) {
103+
nestedBlockingEmitter.init();
104+
while (!nestedBlockingEmitter.isReady()) {
105+
try {
106+
nestedBlockingEmitter.wait();
107+
} catch (InterruptedException e) {
108+
}
109+
}
110+
}
111+
}
112+
113+
private void onProviderEvent(ProviderEvent providerEvent) {
114+
synchronized (nestedBlockingEmitter) {
115+
if (providerEvent == ProviderEvent.PROVIDER_READY) {
116+
nestedBlockingEmitter.setReady();
117+
/*
118+
* This line deadlocked in the original implementation without the emitterExecutor see
119+
* https://github.com/open-feature/java-sdk/issues/1299
120+
*/
121+
emitProviderReady(ProviderEventDetails.builder().build());
122+
}
123+
}
124+
}
125+
126+
@Override
127+
public ProviderEvaluation<Boolean> getBooleanEvaluation(
128+
String key, Boolean defaultValue, EvaluationContext ctx) {
129+
throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'");
130+
}
131+
132+
@Override
133+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
134+
throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'");
135+
}
136+
137+
@Override
138+
public ProviderEvaluation<Integer> getIntegerEvaluation(
139+
String key, Integer defaultValue, EvaluationContext ctx) {
140+
throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'");
141+
}
142+
143+
@Override
144+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
145+
throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'");
146+
}
147+
148+
@Override
149+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
150+
throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'");
151+
}
152+
}
153+
154+
static class NestedBlockingEmitter {
155+
156+
private final Consumer<ProviderEvent> emitProviderEvent;
157+
private volatile boolean isReady;
158+
159+
public NestedBlockingEmitter(Consumer<ProviderEvent> emitProviderEvent) {
160+
this.emitProviderEvent = emitProviderEvent;
161+
}
162+
163+
public void init() {
164+
// run init outside monitored thread
165+
new Thread(() -> {
166+
try {
167+
Thread.sleep(500);
168+
} catch (InterruptedException e) {
169+
throw new RuntimeException(e);
170+
}
171+
172+
emitProviderEvent.accept(ProviderEvent.PROVIDER_READY);
173+
})
174+
.start();
175+
}
176+
177+
public boolean isReady() {
178+
return isReady;
179+
}
180+
181+
public synchronized void setReady() {
182+
isReady = true;
183+
this.notifyAll();
184+
}
185+
}
186+
78187
static class TestEventProvider extends EventProvider {
79188

80189
private static final String NAME = "TestEventProvider";

0 commit comments

Comments
 (0)