Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ public DDEvaluator(final Runnable configCallback) {
public boolean initialize(
final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception {
FeatureFlaggingGateway.addConfigListener(this);
return initializationLatch.await(timeout, unit); // await for initialization
return initializationLatch.await(timeout, unit) || hasConfiguration();
}

@Override
public boolean hasConfiguration() {
return configuration.get() != null;
}

@Override
Expand All @@ -69,8 +74,12 @@ public void shutdown() {
@Override
public void accept(final ServerConfiguration config) {
configuration.set(config);
initializationLatch.countDown();
configCallback.run();
if (config != null) {
initializationLatch.countDown();
configCallback.run();
} else if (initializationLatch.getCount() == 0) {
configCallback.run();
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ interface Evaluator {

boolean initialize(long timeout, TimeUnit timeUnit, EvaluationContext context) throws Exception;

boolean hasConfiguration();

void shutdown();

<T> ProviderEvaluation<T> evaluate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static java.util.concurrent.TimeUnit.SECONDS;

import de.thetaphi.forbiddenapis.SuppressForbidden;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.EventProvider;
import dev.openfeature.sdk.Hook;
Expand All @@ -18,7 +19,7 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -30,7 +31,8 @@ public class Provider extends EventProvider implements Metadata {
private static final Options DEFAULT_OPTIONS = new Options().initTimeout(30, SECONDS);
private volatile Evaluator evaluator;
private final Options options;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final AtomicReference<InitializationState> initializationState =
new AtomicReference<>(InitializationState.NOT_STARTED);
private final FlagEvalMetrics flagEvalMetrics;
private final FlagEvalHook flagEvalHook;

Expand Down Expand Up @@ -61,30 +63,97 @@ public Provider(final Options options) {

@Override
public void initialize(final EvaluationContext context) throws Exception {
initializationState.set(InitializationState.INITIALIZING);
try {
evaluator = buildEvaluator();
final boolean init = evaluator.initialize(options.getTimeout(), options.getUnit(), context);
initialized.set(init);
if (!init) {
if (!evaluator.initialize(options.getTimeout(), options.getUnit(), context)) {
if (markInitialConfigReceivedReady()) {
return;
}
markInitializationError();
throw new ProviderNotReadyError(
"Provider timed-out while waiting for initial configuration");
}
if (!evaluator.hasConfiguration() || !markSuccessfulInitializationReady()) {
markInitializationError();
throw new ProviderNotReadyError(
"Provider timed-out while waiting for initial configuration");
}
} catch (final OpenFeatureError e) {
markInitializationError();
throw e;
} catch (final Throwable e) {
markInitializationError();
throw new FatalError("Failed to initialize provider, is the tracer configured?", e);
}
}

private void onConfigurationChange() {
if (initialized.getAndSet(true)) {
emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().message("New configuration received").build());
} else {
void onConfigurationChange() {
if (evaluator == null || !evaluator.hasConfiguration()) {
onConfigurationUnavailable();
return;
}

final InitializationState state = initializationState.get();
if (state == InitializationState.INITIALIZING) {
initializationState.compareAndSet(
InitializationState.INITIALIZING, InitializationState.INITIAL_CONFIG_RECEIVED);
return;
}
if (state == InitializationState.INITIAL_CONFIG_RECEIVED) {
return;
}
if (state == InitializationState.ERROR
&& initializationState.compareAndSet(
InitializationState.ERROR, InitializationState.READY)) {
emit(
ProviderEvent.PROVIDER_READY,
ProviderEventDetails.builder().message("Provider ready").build());
return;
}
if (initializationState.get() != InitializationState.READY) {
return;
}
emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().message("New configuration received").build());
}

private void onConfigurationUnavailable() {
if (initializationState.compareAndSet(
InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.ERROR)) {
return;
}
if (!initializationState.compareAndSet(InitializationState.READY, InitializationState.ERROR)) {
return;
}
emit(
ProviderEvent.PROVIDER_ERROR,
ProviderEventDetails.builder()
.message("Configuration unavailable")
.errorCode(ErrorCode.PROVIDER_NOT_READY)
.build());
}

private boolean markInitialConfigReceivedReady() {
return initializationState.get() == InitializationState.READY
|| initializationState.compareAndSet(
InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.READY);
}

private boolean markSuccessfulInitializationReady() {
return markInitialConfigReceivedReady()
|| initializationState.compareAndSet(
InitializationState.INITIALIZING, InitializationState.READY);
}

private void markInitializationError() {
InitializationState state = initializationState.get();
while (state != InitializationState.READY && state != InitializationState.ERROR) {
if (initializationState.compareAndSet(state, InitializationState.ERROR)) {
return;
}
state = initializationState.get();
}
}

Expand Down Expand Up @@ -160,6 +229,14 @@ protected Class<?> loadEvaluatorClass() throws ClassNotFoundException {
return Class.forName(EVALUATOR_IMPL);
}

private enum InitializationState {
NOT_STARTED,
INITIALIZING,
INITIAL_CONFIG_RECEIVED,
READY,
ERROR
}

public static class Options {

private long timeout;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -52,6 +54,9 @@
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -79,6 +84,7 @@ public void setup() {
@AfterEach
public void tearDown() {
FeatureFlaggingGateway.removeExposureListener(exposureListener);
FeatureFlaggingGateway.dispatch((ServerConfiguration) null);
}

private static Arguments[] valueMappingTestCases() {
Expand Down Expand Up @@ -148,6 +154,39 @@ public void testEvaluateNoConfig() {
assertThat(details.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY));
}

@Test
public void testInitializeTimesOutWithoutConfig() throws Exception {
final Runnable configCallback = mock(Runnable.class);
final DDEvaluator evaluator = new DDEvaluator(configCallback);
evaluator.accept(null);
try {
assertThat(
evaluator.initialize(10, MILLISECONDS, mock(EvaluationContext.class)), equalTo(false));
verify(configCallback, times(0)).run();
} finally {
evaluator.shutdown();
}
}

@Test
public void testInitializeWaitsForNonNullConfig() throws Exception {
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));
final ExecutorService executor = Executors.newSingleThreadExecutor();
try {
final Future<Boolean> initialized =
executor.submit(() -> evaluator.initialize(1, SECONDS, mock(EvaluationContext.class)));

evaluator.accept(null);
assertThat(initialized.isDone(), equalTo(false));

evaluator.accept(mock(ServerConfiguration.class));
assertThat(initialized.get(1, SECONDS), equalTo(true));
} finally {
executor.shutdownNow();
evaluator.shutdown();
}
}

@Test
public void testEvaluateNoContext() {
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));
Expand Down
Loading