Skip to content

Commit ecab9a7

Browse files
Add timeouts to provider initialization
1 parent d3dafc5 commit ecab9a7

File tree

4 files changed

+118
-40
lines changed

4 files changed

+118
-40
lines changed

products/openfeature/src/main/java/datadog/trace/api/openfeature/DDFeatureFlagEvaluator.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.Objects;
4141
import java.util.Set;
4242
import java.util.concurrent.CountDownLatch;
43+
import java.util.concurrent.TimeUnit;
4344
import java.util.concurrent.atomic.AtomicReference;
4445
import java.util.regex.Pattern;
4546
import org.slf4j.Logger;
@@ -64,9 +65,10 @@ public DDFeatureFlagEvaluator(final Runnable configCallback) {
6465
}
6566

6667
@Override
67-
public void initialize(final EvaluationContext context) throws Exception {
68+
public boolean initialize(
69+
final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception {
6870
FeatureFlagGateway.addConfigListener(this);
69-
initializationLatch.await(); // await for initialization
71+
return initializationLatch.await(timeout, unit); // await for initialization
7072
}
7173

7274
@Override

products/openfeature/src/main/java/datadog/trace/api/openfeature/FeatureFlagEvaluator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import dev.openfeature.sdk.EvaluationContext;
44
import dev.openfeature.sdk.ProviderEvaluation;
5+
import java.util.concurrent.TimeUnit;
56

67
interface FeatureFlagEvaluator {
78

8-
void initialize(EvaluationContext context) throws Exception;
9+
boolean initialize(long timeout, TimeUnit timeUnit, EvaluationContext context) throws Exception;
910

1011
void shutdown();
1112

products/openfeature/src/main/java/datadog/trace/api/openfeature/Provider.java

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package datadog.trace.api.openfeature;
22

3+
import static java.util.concurrent.TimeUnit.SECONDS;
4+
35
import de.thetaphi.forbiddenapis.SuppressForbidden;
46
import dev.openfeature.sdk.EvaluationContext;
57
import dev.openfeature.sdk.EventProvider;
@@ -8,38 +10,62 @@
810
import dev.openfeature.sdk.ProviderEvent;
911
import dev.openfeature.sdk.ProviderEventDetails;
1012
import dev.openfeature.sdk.Value;
13+
import dev.openfeature.sdk.exceptions.FatalError;
14+
import dev.openfeature.sdk.exceptions.OpenFeatureError;
15+
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
1116
import java.lang.reflect.Constructor;
12-
import org.slf4j.Logger;
13-
import org.slf4j.LoggerFactory;
17+
import java.util.concurrent.TimeUnit;
18+
import java.util.concurrent.atomic.AtomicBoolean;
1419

1520
public class Provider extends EventProvider implements Metadata {
1621

17-
private static final Logger LOGGER = LoggerFactory.getLogger(Provider.class);
1822
static final String METADATA = "datadog-openfeature-provider";
1923
private static final String EVALUATOR_IMPL =
2024
"datadog.trace.api.openfeature.DDFeatureFlagEvaluator";
21-
private FeatureFlagEvaluator evaluator;
25+
private static final Options DEFAULT_OPTIONS = new Options().initTimeout(30, SECONDS);
26+
private volatile FeatureFlagEvaluator evaluator;
27+
private final Options options;
28+
private final AtomicBoolean initialized = new AtomicBoolean(false);
2229

2330
public Provider() {
24-
this(null);
31+
this(DEFAULT_OPTIONS, null);
32+
}
33+
34+
public Provider(final Options options) {
35+
this(options, null);
2536
}
2637

27-
Provider(final FeatureFlagEvaluator evaluator) {
38+
Provider(final Options options, final FeatureFlagEvaluator evaluator) {
39+
this.options = options;
2840
this.evaluator = evaluator;
2941
}
3042

3143
@Override
3244
public void initialize(final EvaluationContext context) throws Exception {
3345
try {
3446
evaluator = buildEvaluator();
35-
evaluator.initialize(context);
47+
final boolean init = evaluator.initialize(options.getTimeout(), options.getUnit(), context);
48+
initialized.set(init);
49+
if (!init) {
50+
throw new ProviderNotReadyError(
51+
"Provider timed-out while waiting for initial configuration");
52+
}
53+
} catch (final OpenFeatureError e) {
54+
throw e;
55+
} catch (final Throwable e) {
56+
throw new FatalError("Failed to initialize provider, is the tracer configured?", e);
57+
}
58+
}
59+
60+
private void onConfigurationChange() {
61+
if (initialized.getAndSet(true)) {
62+
emit(
63+
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
64+
ProviderEventDetails.builder().message("New configuration received").build());
65+
} else {
3666
emit(
3767
ProviderEvent.PROVIDER_READY,
3868
ProviderEventDetails.builder().message("Provider ready").build());
39-
} catch (final Throwable e) {
40-
final String message = "Failed to initialize Datadog provider, is the tracer configured?";
41-
LOGGER.error(message, e);
42-
emit(ProviderEvent.PROVIDER_ERROR, ProviderEventDetails.builder().message(message).build());
4369
}
4470
}
4571

@@ -49,17 +75,14 @@ private FeatureFlagEvaluator buildEvaluator() throws Exception {
4975
}
5076
final Class<?> evaluatorClass = loadEvaluatorClass();
5177
final Constructor<?> ctor = evaluatorClass.getConstructor(Runnable.class);
52-
final Runnable configChangeListener =
53-
() ->
54-
emit(
55-
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
56-
ProviderEventDetails.builder().message("New configuration received").build());
57-
return (FeatureFlagEvaluator) ctor.newInstance(configChangeListener);
78+
return (FeatureFlagEvaluator) ctor.newInstance((Runnable) this::onConfigurationChange);
5879
}
5980

6081
@Override
6182
public void shutdown() {
62-
evaluator.shutdown();
83+
if (evaluator != null) {
84+
evaluator.shutdown();
85+
}
6386
}
6487

6588
@Override
@@ -106,4 +129,24 @@ public ProviderEvaluation<Value> getObjectEvaluation(
106129
protected Class<?> loadEvaluatorClass() throws ClassNotFoundException {
107130
return Class.forName(EVALUATOR_IMPL);
108131
}
132+
133+
public static class Options {
134+
135+
private long timeout;
136+
private TimeUnit unit;
137+
138+
public Options initTimeout(final long timeout, final TimeUnit unit) {
139+
this.timeout = timeout;
140+
this.unit = unit;
141+
return this;
142+
}
143+
144+
public long getTimeout() {
145+
return timeout;
146+
}
147+
148+
public TimeUnit getUnit() {
149+
return unit;
150+
}
151+
}
109152
}

products/openfeature/src/test/java/datadog/trace/api/openfeature/ProviderTest.java

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import static datadog.trace.api.openfeature.Provider.METADATA;
44
import static java.time.Duration.ofSeconds;
5+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
6+
import static java.util.concurrent.TimeUnit.SECONDS;
57
import static org.awaitility.Awaitility.await;
68
import static org.hamcrest.CoreMatchers.equalTo;
79
import static org.hamcrest.MatcherAssert.assertThat;
10+
import static org.junit.jupiter.api.Assertions.assertThrows;
811
import static org.mockito.ArgumentMatchers.any;
12+
import static org.mockito.ArgumentMatchers.anyLong;
913
import static org.mockito.ArgumentMatchers.eq;
1014
import static org.mockito.Mockito.mock;
1115
import static org.mockito.Mockito.times;
@@ -14,15 +18,19 @@
1418

1519
import datadog.trace.api.featureflag.FeatureFlagGateway;
1620
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
21+
import datadog.trace.api.openfeature.Provider.Options;
1722
import dev.openfeature.sdk.Client;
1823
import dev.openfeature.sdk.EvaluationContext;
1924
import dev.openfeature.sdk.EventDetails;
2025
import dev.openfeature.sdk.Features;
2126
import dev.openfeature.sdk.FlagEvaluationDetails;
2227
import dev.openfeature.sdk.OpenFeatureAPI;
2328
import dev.openfeature.sdk.ProviderEvaluation;
29+
import dev.openfeature.sdk.ProviderEvent;
2430
import dev.openfeature.sdk.ProviderState;
2531
import dev.openfeature.sdk.Value;
32+
import dev.openfeature.sdk.exceptions.FatalError;
33+
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
2634
import java.util.concurrent.ExecutorService;
2735
import java.util.concurrent.Executors;
2836
import java.util.function.Consumer;
@@ -81,32 +89,55 @@ public void testSetProviderAndWait() {
8189
}
8290

8391
@Test
84-
public void testFailureToLoadInternalApi() {
85-
@SuppressWarnings("unchecked")
86-
final Consumer<EventDetails> consumer = mock(Consumer.class);
87-
92+
public void testSetProviderAndWaitTimeout() {
93+
final Consumer<EventDetails> readyEvent = mock(Consumer.class);
8894
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
89-
api.onProviderError(consumer);
90-
api.setProviderAndWait(
91-
new Provider() {
92-
@Override
93-
protected Class<?> loadEvaluatorClass() throws ClassNotFoundException {
94-
throw new ClassNotFoundException(
95-
"Class " + FeatureFlagGateway.class.getName() + " not found");
96-
}
97-
});
95+
final Client client = api.getClient();
96+
client.on(ProviderEvent.PROVIDER_READY, readyEvent);
97+
98+
// we time out after 10 millis without receiving the initial config
99+
assertThrows(
100+
ProviderNotReadyError.class,
101+
() -> api.setProviderAndWait(new Provider(new Options().initTimeout(10, MILLISECONDS))));
102+
103+
// ready has not yet been called
104+
verify(readyEvent, times(0)).accept(any());
105+
106+
// dispatch an initial configuration
107+
FeatureFlagGateway.dispatch(mock(ServerConfiguration.class));
98108

109+
// ready is called after receiving the configuration
99110
await()
100111
.atMost(ofSeconds(1))
101112
.untilAsserted(
102113
() -> {
103-
verify(consumer, times(1)).accept(eventDetailsCaptor.capture());
104-
final EventDetails eventDetails = eventDetailsCaptor.getValue();
105-
assertThat(eventDetails.getProviderName(), equalTo(METADATA));
106-
assertThat(api.getClient().getProviderState(), equalTo(ProviderState.ERROR));
114+
verify(readyEvent, times(1)).accept(eventDetailsCaptor.capture());
115+
final EventDetails details = eventDetailsCaptor.getValue();
116+
assertThat(details.getProviderName(), equalTo(METADATA));
107117
});
108118
}
109119

120+
@Test
121+
public void testFailureToLoadInternalApi() {
122+
@SuppressWarnings("unchecked")
123+
final Consumer<EventDetails> consumer = mock(Consumer.class);
124+
125+
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
126+
api.onProviderError(consumer);
127+
128+
assertThrows(
129+
FatalError.class,
130+
() ->
131+
api.setProviderAndWait(
132+
new Provider() {
133+
@Override
134+
protected Class<?> loadEvaluatorClass() throws ClassNotFoundException {
135+
throw new ClassNotFoundException(
136+
"Class " + FeatureFlagGateway.class.getName() + " not found");
137+
}
138+
}));
139+
}
140+
110141
public interface EvaluateMethod<E> {
111142
FlagEvaluationDetails<E> evaluate(Features client, String flag, E defaultValue);
112143
}
@@ -127,6 +158,7 @@ public <E> void testProviderEvaluation(
127158
final String flag, final E defaultValue, final EvaluateMethod<E> method) throws Exception {
128159
FeatureFlagGateway.dispatch(mock(ServerConfiguration.class));
129160
final FeatureFlagEvaluator evaluator = mock(FeatureFlagEvaluator.class);
161+
when(evaluator.initialize(anyLong(), any(), any())).thenReturn(true);
130162
when(evaluator.evaluate(any(), any(), any(), any()))
131163
.thenAnswer(
132164
invocation ->
@@ -135,12 +167,12 @@ public <E> void testProviderEvaluation(
135167
.reason("MOCK")
136168
.build());
137169
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
138-
api.setProviderAndWait(new Provider(evaluator));
170+
api.setProviderAndWait(new Provider(new Options().initTimeout(10, SECONDS), evaluator));
139171
final Client client = api.getClient();
140172
final FlagEvaluationDetails<E> result = method.evaluate(client, flag, defaultValue);
141173
assertThat(result.getValue(), equalTo(defaultValue));
142174
assertThat(result.getReason(), equalTo("MOCK"));
143-
verify(evaluator, times(1)).initialize(any());
175+
verify(evaluator, times(1)).initialize(eq(10L), eq(SECONDS), any());
144176
verify(evaluator, times(1))
145177
.evaluate(any(), eq(flag), eq(defaultValue), any(EvaluationContext.class));
146178
}

0 commit comments

Comments
 (0)