diff --git a/hooks/open-telemetry/pom.xml b/hooks/open-telemetry/pom.xml index b1bc64fa0..970123fc4 100644 --- a/hooks/open-telemetry/pom.xml +++ b/hooks/open-telemetry/pom.xml @@ -38,13 +38,6 @@ - - dev.openfeature - sdk - [1.4,2.0) - provided - - io.opentelemetry opentelemetry-semconv diff --git a/pom.xml b/pom.xml index c52f269ab..0220690be 100644 --- a/pom.xml +++ b/pom.xml @@ -62,10 +62,11 @@ + dev.openfeature sdk - - [1.0,2.0) + + [1.12,2.0) provided diff --git a/providers/configcat/src/main/java/dev/openfeature/contrib/providers/configcat/ConfigCatProvider.java b/providers/configcat/src/main/java/dev/openfeature/contrib/providers/configcat/ConfigCatProvider.java index bb6ef2e15..b34fec746 100644 --- a/providers/configcat/src/main/java/dev/openfeature/contrib/providers/configcat/ConfigCatProvider.java +++ b/providers/configcat/src/main/java/dev/openfeature/contrib/providers/configcat/ConfigCatProvider.java @@ -1,24 +1,23 @@ package dev.openfeature.contrib.providers.configcat; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + import com.configcat.ConfigCatClient; import com.configcat.EvaluationDetails; import com.configcat.User; + import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - /** * Provider implementation for ConfigCat. */ @@ -36,9 +35,6 @@ public class ConfigCatProvider extends EventProvider { @Getter private ConfigCatClient configCatClient; - @Getter - private ProviderState state = ProviderState.NOT_READY; - private AtomicBoolean isInitialized = new AtomicBoolean(false); /** @@ -64,8 +60,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { configCatClient = ConfigCatClient.get(configCatProviderConfig.getSdkKey(), configCatProviderConfig.getOptions()); configCatProviderConfig.postInit(); - state = ProviderState.READY; - log.info("finished initializing provider, state: {}", state); + log.info("finished initializing provider"); configCatClient.getHooks().addOnClientReady(() -> { ProviderEventDetails providerEventDetails = ProviderEventDetails.builder() @@ -123,12 +118,6 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa private ProviderEvaluation getEvaluation(Class classOfT, String key, T defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); - } - throw new GeneralError(UNKNOWN_ERROR); - } User user = ctx == null ? null : ContextTransformer.transform(ctx); EvaluationDetails evaluationDetails; T evaluatedValue; @@ -157,6 +146,5 @@ public void shutdown() { if (configCatClient != null) { configCatClient.close(); } - state = ProviderState.NOT_READY; } } diff --git a/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java b/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java index 7eb99b375..685089109 100644 --- a/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java +++ b/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java @@ -1,27 +1,26 @@ package dev.openfeature.contrib.providers.configcat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + import com.configcat.OverrideBehaviour; import com.configcat.OverrideDataSourceBuilder; import com.configcat.User; + import dev.openfeature.sdk.Client; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.ProviderEventDetails; import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; /** * ConfigCatProvider test, based on local config file evaluation. @@ -181,27 +180,6 @@ void getStringEvaluationByUser() { assertEquals("fallback", client.getStringValue(USERS_FLAG_NAME + "Str", "111", evaluationContext)); } - @SneakyThrows - @Test - void shouldThrowIfNotInitialized() { - ConfigCatProviderConfig configCatProviderConfig = ConfigCatProviderConfig.builder().sdkKey("configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012").build(); - ConfigCatProvider tempConfigCatProvider = new ConfigCatProvider(configCatProviderConfig); - - assertThrows(ProviderNotReadyError.class, ()-> tempConfigCatProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); - - OpenFeatureAPI.getInstance().setProviderAndWait("tempConfigCatProvider", tempConfigCatProvider); - - assertThrows(GeneralError.class, ()-> tempConfigCatProvider.initialize(null)); - - tempConfigCatProvider.shutdown(); - - assertThrows(ProviderNotReadyError.class, ()-> tempConfigCatProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempConfigCatProvider.getDoubleEvaluation("fail_not_initialized", 0.1, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempConfigCatProvider.getIntegerEvaluation("fail_not_initialized", 3, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempConfigCatProvider.getObjectEvaluation("fail_not_initialized", null, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempConfigCatProvider.getStringEvaluation("fail_not_initialized", "", new ImmutableContext())); - } - @Test void eventsTest() { configCatProvider.emitProviderReady(ProviderEventDetails.builder().build()); diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml index 43c8163c0..c93a55a8a 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -32,16 +32,7 @@ - - - - dev.openfeature - sdk - [1.4,2.0) - provided - - io.grpc grpc-netty diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java index 58b4f2e6b..a396d00f8 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java @@ -1,5 +1,7 @@ package dev.openfeature.contrib.providers.flagd; +import java.util.List; + import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; @@ -9,27 +11,19 @@ import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import lombok.extern.slf4j.Slf4j; -import java.util.List; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - /** * OpenFeature provider for flagd. */ @Slf4j -@SuppressWarnings({"PMD.TooManyStaticImports", "checkstyle:NoFinalizer"}) +@SuppressWarnings({ "PMD.TooManyStaticImports", "checkstyle:NoFinalizer" }) public class FlagdProvider extends EventProvider { private static final String FLAGD_PROVIDER = "flagD Provider"; - - private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Resolver flagResolver; - private ProviderState state = ProviderState.NOT_READY; - private boolean initialized = false; + private volatile boolean initialized = false; + private volatile boolean connected = false; private EvaluationContext evaluationContext; @@ -52,14 +46,14 @@ public FlagdProvider() { public FlagdProvider(final FlagdOptions options) { switch (options.getResolverType().asString()) { case Config.RESOLVER_IN_PROCESS: - this.flagResolver = new InProcessResolver(options, this::setState); + this.flagResolver = new InProcessResolver(options, this::isConnected, + this::onResolverConnectionChanged); break; case Config.RESOLVER_RPC: - this.flagResolver = - new GrpcResolver(options, - new Cache(options.getCacheType(), options.getMaxCacheSize()), - this::getState, - this::setState); + this.flagResolver = new GrpcResolver(options, + new Cache(options.getCacheType(), options.getMaxCacheSize()), + this::isConnected, + this::onResolverConnectionChanged); break; default: throw new IllegalStateException( @@ -86,24 +80,13 @@ public synchronized void shutdown() { try { this.flagResolver.shutdown(); - this.initialized = false; } catch (Exception e) { log.error("Error during shutdown {}", FLAGD_PROVIDER, e); - } - } - - @Override - public ProviderState getState() { - Lock l = this.lock.readLock(); - try { - l.lock(); - return this.state; } finally { - l.unlock(); + this.initialized = false; } } - @Override public Metadata getMetadata() { return () -> FLAGD_PROVIDER; @@ -142,34 +125,17 @@ private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) { return clientCallCtx; } - private void setState(ProviderState newState, List changedFlagsKeys) { - ProviderState oldState; - Lock l = this.lock.writeLock(); - try { - l.lock(); - oldState = this.state; - this.state = newState; - } finally { - l.unlock(); - } - this.handleStateTransition(oldState, newState, changedFlagsKeys); + private boolean isConnected() { + return this.connected; } - private void handleStateTransition(ProviderState oldState, ProviderState newState, List changedFlagKeys) { - // we got initialized - if (ProviderState.NOT_READY.equals(oldState) && ProviderState.READY.equals(newState)) { - // nothing to do, the SDK emits the events - log.debug("Init completed"); - return; - } - // we got shutdown, not checking oldState as behavior remains the same for shutdown - if (ProviderState.NOT_READY.equals(newState)) { - // nothing to do - log.debug("shutdown completed"); - return; - } + private void onResolverConnectionChanged(boolean newConnectedState, List changedFlagKeys) { + boolean previous = connected; + boolean current = newConnectedState; + this.connected = newConnectedState; + // configuration changed - if (ProviderState.READY.equals(oldState) && ProviderState.READY.equals(newState)) { + if (initialized && previous && current) { log.debug("Configuration changed"); ProviderEventDetails details = ProviderEventDetails.builder().flagsChanged(changedFlagKeys) .message("configuration changed").build(); @@ -177,14 +143,14 @@ private void handleStateTransition(ProviderState oldState, ProviderState newStat return; } // there was an error - if (ProviderState.READY.equals(oldState) && ProviderState.ERROR.equals(newState)) { + if (initialized && previous && !current) { log.debug("There has been an error"); ProviderEventDetails details = ProviderEventDetails.builder().message("there has been an error").build(); this.emitProviderError(details); return; } - // we recover from an error - if (ProviderState.ERROR.equals(oldState) && ProviderState.READY.equals(newState)) { + // we recovered from an error + if (initialized && !previous && current) { log.debug("Recovered from error"); ProviderEventDetails details = ProviderEventDetails.builder().message("recovered from error").build(); this.emitProviderReady(details); diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java index cec6dacca..3f9d8981f 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java @@ -1,6 +1,6 @@ package dev.openfeature.contrib.providers.flagd.resolver.common; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import dev.openfeature.sdk.exceptions.GeneralError; @@ -14,20 +14,23 @@ private Util() { /** * A helper to block the caller for given conditions. - * - * @param deadline number of milliseconds to block - * @param check {@link AtomicBoolean} to check for status true + * + * @param deadline number of milliseconds to block + * @param connectedSupplier func to check for status true + * @throws InterruptedException if interrupted */ - public static void busyWaitAndCheck(final Long deadline, final AtomicBoolean check) throws InterruptedException { + public static void busyWaitAndCheck(final Long deadline, final Supplier connectedSupplier) + throws InterruptedException { long start = System.currentTimeMillis(); do { if (deadline <= System.currentTimeMillis() - start) { throw new GeneralError( - String.format("Deadline exceeded. Condition did not complete within the %d deadline", deadline)); + String.format("Deadline exceeded. Condition did not complete within the %d deadline", + deadline)); } Thread.sleep(50L); - } while (!check.get()); + } while (!connectedSupplier.get()); } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java index b749adfd2..ed279da8e 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java @@ -1,26 +1,26 @@ package dev.openfeature.contrib.providers.flagd.resolver.grpc; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + import com.google.protobuf.Value; + import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse; -import dev.openfeature.sdk.ProviderState; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; - /** * EventStreamObserver handles events emitted by flagd. */ @Slf4j @SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects") class EventStreamObserver implements StreamObserver { - private final BiConsumer> stateConsumer; + private final BiConsumer> stateConsumer; private final Object sync; private final Cache cache; @@ -35,7 +35,7 @@ class EventStreamObserver implements StreamObserver { * @param cache cache to update * @param stateConsumer lambda to call for setting the state */ - EventStreamObserver(Object sync, Cache cache, BiConsumer> stateConsumer) { + EventStreamObserver(Object sync, Cache cache, BiConsumer> stateConsumer) { this.sync = sync; this.cache = cache; this.stateConsumer = stateConsumer; @@ -61,7 +61,7 @@ public void onError(Throwable t) { if (this.cache.getEnabled()) { this.cache.clear(); } - this.stateConsumer.accept(ProviderState.ERROR, Collections.emptyList()); + this.stateConsumer.accept(false, Collections.emptyList()); // handle last call of this stream handleEndOfStream(); @@ -72,7 +72,7 @@ public void onCompleted() { if (this.cache.getEnabled()) { this.cache.clear(); } - this.stateConsumer.accept(ProviderState.ERROR, Collections.emptyList()); + this.stateConsumer.accept(false, Collections.emptyList()); // handle last call of this stream handleEndOfStream(); @@ -99,11 +99,11 @@ private void handleConfigurationChangeEvent(EventStreamResponse value) { } } - this.stateConsumer.accept(ProviderState.READY, changedFlags); + this.stateConsumer.accept(true, changedFlags); } private void handleProviderReadyEvent() { - this.stateConsumer.accept(ProviderState.READY, Collections.emptyList()); + this.stateConsumer.accept(true, Collections.emptyList()); if (this.cache.getEnabled()) { this.cache.clear(); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java index 08de59824..563bad739 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java @@ -4,8 +4,9 @@ import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; +import java.util.function.Supplier; + import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelBuilder; import dev.openfeature.contrib.providers.flagd.resolver.common.Util; @@ -13,7 +14,6 @@ import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamRequest; import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; -import dev.openfeature.sdk.ProviderState; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.grpc.ManagedChannel; import io.grpc.stub.StreamObserver; @@ -26,7 +26,6 @@ @SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects") public class GrpcConnector { private final Object sync = new Object(); - private final AtomicBoolean connected = new AtomicBoolean(false); private final Random random = new Random(); private final ServiceGrpc.ServiceBlockingStub serviceBlockingStub; @@ -38,7 +37,8 @@ public class GrpcConnector { private final long deadline; private final Cache cache; - private final BiConsumer> stateConsumer; + private final BiConsumer> stateConsumer; + private final Supplier connectedSupplier; private int eventStreamAttempt = 1; private int eventStreamRetryBackoff; @@ -53,8 +53,8 @@ public class GrpcConnector { * @param cache cache to use. * @param stateConsumer lambda to call for setting the state. */ - public GrpcConnector(final FlagdOptions options, final Cache cache, - BiConsumer> stateConsumer) { + public GrpcConnector(final FlagdOptions options, final Cache cache, final Supplier connectedSupplier, + BiConsumer> stateConsumer) { this.channel = ChannelBuilder.nettyChannel(options); this.serviceStub = ServiceGrpc.newStub(channel); this.serviceBlockingStub = ServiceGrpc.newBlockingStub(channel); @@ -65,6 +65,7 @@ public GrpcConnector(final FlagdOptions options, final Cache cache, this.deadline = options.getDeadline(); this.cache = cache; this.stateConsumer = stateConsumer; + this.connectedSupplier = connectedSupplier; } /** @@ -76,7 +77,7 @@ public void initialize() throws Exception { eventObserverThread.start(); // block till ready - Util.busyWaitAndCheck(this.deadline, this.connected); + Util.busyWaitAndCheck(this.deadline, this.connectedSupplier); } /** @@ -103,7 +104,7 @@ public void shutdown() throws Exception { this.channel.awaitTermination(this.deadline, TimeUnit.MILLISECONDS); log.warn(String.format("Unable to shut down channel by %d deadline", this.deadline)); } - this.stateConsumer.accept(ProviderState.NOT_READY, Collections.emptyList()); + this.stateConsumer.accept(false, Collections.emptyList()); } } @@ -154,21 +155,16 @@ private void observeEventStream() { } log.error("failed to connect to event stream, exhausted retries"); - this.grpcStateConsumer(ProviderState.ERROR, null); + this.grpcStateConsumer(false, null); } - private void grpcStateConsumer(final ProviderState state, final List changedFlags) { - // check for readiness - if (ProviderState.READY.equals(state)) { + private void grpcStateConsumer(final boolean connected, final List changedFlags) { + // reset reconnection states + if (connected) { this.eventStreamAttempt = 1; this.eventStreamRetryBackoff = this.startEventStreamRetryBackoff; - this.connected.set(true); - } else if (ProviderState.ERROR.equals(state)) { - // reset connection status - this.connected.set(false); } - // chain to initiator - this.stateConsumer.accept(state, changedFlags); + this.stateConsumer.accept(connected, changedFlags); } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java index 06b7470cc..7226d7257 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java @@ -1,7 +1,5 @@ package dev.openfeature.contrib.providers.flagd.resolver.grpc; -import dev.openfeature.contrib.providers.flagd.Config; - import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,6 +14,7 @@ import com.google.protobuf.NullValue; import com.google.protobuf.Struct; +import dev.openfeature.contrib.providers.flagd.Config; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; @@ -30,7 +29,6 @@ import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.MutableStructure; import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.GeneralError; @@ -51,23 +49,23 @@ public final class GrpcResolver implements Resolver { private final GrpcConnector connector; private final Cache cache; private final ResolveStrategy strategy; - private final Supplier stateSupplier; + private final Supplier connectedSupplier; /** * Initialize Grpc resolver. * * @param options flagd options. * @param cache cache to use. - * @param stateSupplier lambda to call for getting the state. - * @param stateConsumer lambda to communicate back the state. + * @param connectedSupplier lambda to call for getting the state. + * @param onResolverConnectionChanged lambda to communicate back the state. */ - public GrpcResolver(final FlagdOptions options, final Cache cache, final Supplier stateSupplier, - final BiConsumer> stateConsumer) { + public GrpcResolver(final FlagdOptions options, final Cache cache, final Supplier connectedSupplier, + final BiConsumer> onResolverConnectionChanged) { this.cache = cache; - this.stateSupplier = stateSupplier; + this.connectedSupplier = connectedSupplier; this.strategy = ResolveFactory.getStrategy(options); - this.connector = new GrpcConnector(options, cache, stateConsumer); + this.connector = new GrpcConnector(options, cache, connectedSupplier, onResolverConnectionChanged); } /** @@ -199,7 +197,7 @@ private Boolean isEvaluationCacheable(ProviderEvaluation evaluation) { } private Boolean cacheAvailable() { - return this.cache.getEnabled() && ProviderState.READY.equals(this.stateSupplier.get()); + return this.cache.getEnabled() && this.connectedSupplier.get(); } /** diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index a576443af..06e641736 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -1,5 +1,11 @@ package dev.openfeature.contrib.providers.flagd.resolver.process; +import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.common.Util; @@ -16,19 +22,12 @@ import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.extern.slf4j.Slf4j; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BiConsumer; - -import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING; - /** * flagd in-process resolver. Resolves feature flags in-process. Flags are * retrieved from {@link Storage}, where the @@ -37,20 +36,25 @@ @Slf4j public class InProcessResolver implements Resolver { private final Storage flagStore; - private final BiConsumer> stateConsumer; + private final BiConsumer> onResolverConnectionChanged; private final Operator operator; private final long deadline; private final ImmutableMetadata metadata; - private final AtomicBoolean connected = new AtomicBoolean(false); + private final Supplier connectedSupplier; /** * Initialize an in-process resolver. + * @param options flagd options + * @param connectedSupplier supplier for connection state + * @param onResolverConnectionChanged handler for connection change */ - public InProcessResolver(FlagdOptions options, BiConsumer> stateConsumer) { + public InProcessResolver(FlagdOptions options, final Supplier connectedSupplier, + BiConsumer> onResolverConnectionChanged) { this.flagStore = new FlagStore(getConnector(options)); this.deadline = options.getDeadline(); - this.stateConsumer = stateConsumer; + this.onResolverConnectionChanged = onResolverConnectionChanged; this.operator = new Operator(); + this.connectedSupplier = connectedSupplier; this.metadata = options.getSelector() == null ? null : ImmutableMetadata.builder() .addString("scope", options.getSelector()) @@ -68,15 +72,11 @@ public void init() throws Exception { final StorageStateChange storageStateChange = flagStore.getStateQueue().take(); switch (storageStateChange.getStorageState()) { case OK: - stateConsumer.accept(ProviderState.READY, storageStateChange.getChangedFlagsKeys()); - this.connected.set(true); + onResolverConnectionChanged.accept(true, storageStateChange.getChangedFlagsKeys()); break; case ERROR: - stateConsumer.accept(ProviderState.ERROR, null); - this.connected.set(false); + onResolverConnectionChanged.accept(false, null); break; - case STALE: - // todo set stale state default: log.info(String.format("Storage emitted unhandled status: %s", storageStateChange.getStorageState())); @@ -91,7 +91,7 @@ public void init() throws Exception { stateWatcher.start(); // block till ready - Util.busyWaitAndCheck(this.deadline, this.connected); + Util.busyWaitAndCheck(this.deadline, this.connectedSupplier); } /** @@ -101,8 +101,7 @@ public void init() throws Exception { */ public void shutdown() throws InterruptedException { flagStore.shutdown(); - this.connected.set(false); - stateConsumer.accept(ProviderState.NOT_READY, null); + onResolverConnectionChanged.accept(false, null); } /** @@ -165,9 +164,9 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC // missing flag if (flag == null) { return ProviderEvaluation.builder() - .errorMessage("flag: " + key + " not found") - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); + .errorMessage("flag: " + key + " not found") + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); } // state check diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java index 1a219f099..f1318ac4b 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java @@ -9,8 +9,10 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -22,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.concurrent.LinkedBlockingQueue; @@ -82,10 +85,10 @@ class FlagdProviderTest { private static final Double DOUBLE_VALUE = .5d; private static final String INNER_STRUCT_KEY = "inner_key"; private static final String INNER_STRUCT_VALUE = "inner_value"; - private static final com.google.protobuf.Struct PROTOBUF_STRUCTURE_VALUE = - Struct.newBuilder().putFields(INNER_STRUCT_KEY, - com.google.protobuf.Value.newBuilder().setStringValue(INNER_STRUCT_VALUE).build()) - .build(); + private static final com.google.protobuf.Struct PROTOBUF_STRUCTURE_VALUE = Struct.newBuilder() + .putFields(INNER_STRUCT_KEY, + com.google.protobuf.Value.newBuilder().setStringValue(INNER_STRUCT_VALUE).build()) + .build(); private static final String STRING_VALUE = "hi!"; private static OpenFeatureAPI api; @@ -150,7 +153,7 @@ void resolvers_call_grpc_service_and_return_details() { GrpcConnector grpc = mock(GrpcConnector.class); when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - OpenFeatureAPI.getInstance().setProvider(createProvider(grpc)); + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertTrue(booleanDetails.getValue()); @@ -223,7 +226,7 @@ void zero_value() { GrpcConnector grpc = mock(GrpcConnector.class); when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - OpenFeatureAPI.getInstance().setProvider(createProvider(grpc)); + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertEquals(false, booleanDetails.getValue()); @@ -274,7 +277,6 @@ void test_metadata_from_grpc_response() { .setMetadata(metadataStruct) .build(); - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))).thenReturn( @@ -284,7 +286,7 @@ void test_metadata_from_grpc_response() { GrpcConnector grpc = mock(GrpcConnector.class); when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - OpenFeatureAPI.getInstance().setProvider(createProvider(grpc)); + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); // when FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); @@ -299,17 +301,17 @@ void test_metadata_from_grpc_response() { @Test void resolvers_cache_responses_if_static_and_event_stream_alive() { - do_resolvers_cache_responses(STATIC_REASON, ProviderState.READY, true); + do_resolvers_cache_responses(STATIC_REASON, true, true); } @Test void resolvers_should_not_cache_responses_if_not_static() { - do_resolvers_cache_responses(DEFAULT.toString(), ProviderState.READY, false); + do_resolvers_cache_responses(DEFAULT.toString(), true, false); } @Test void resolvers_should_not_cache_responses_if_event_stream_not_alive() { - do_resolvers_cache_responses(STATIC_REASON, ProviderState.ERROR, false); + do_resolvers_cache_responses(STATIC_REASON, false, false); } @Test @@ -345,28 +347,26 @@ void context_is_parsed_and_passed_to_grpc_service() { when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) .thenReturn(serviceBlockingStubMock); when(serviceBlockingStubMock.resolveBoolean(argThat( - x -> { - final Struct struct = x.getContext(); - final Map valueMap = struct.getFieldsMap(); - - return STRING_ATTR_VALUE.equals(valueMap.get(STRING_ATTR_KEY).getStringValue()) - && INT_ATTR_VALUE == valueMap.get(INT_ATTR_KEY).getNumberValue() - && DOUBLE_ATTR_VALUE == valueMap.get(DOUBLE_ATTR_KEY).getNumberValue() - && valueMap.get(BOOLEAN_ATTR_KEY).getBoolValue() - && "MY_TARGETING_KEY".equals(valueMap.get("targetingKey").getStringValue()) - && LIST_ATTR_VALUE.get(0).asInteger() == - valueMap.get(LIST_ATTR_KEY).getListValue().getValuesList().get(0).getNumberValue() - && STRUCT_ATTR_INNER_VALUE.equals( + x -> { + final Struct struct = x.getContext(); + final Map valueMap = struct.getFieldsMap(); + + return STRING_ATTR_VALUE.equals(valueMap.get(STRING_ATTR_KEY).getStringValue()) + && INT_ATTR_VALUE == valueMap.get(INT_ATTR_KEY).getNumberValue() + && DOUBLE_ATTR_VALUE == valueMap.get(DOUBLE_ATTR_KEY).getNumberValue() + && valueMap.get(BOOLEAN_ATTR_KEY).getBoolValue() + && "MY_TARGETING_KEY".equals(valueMap.get("targetingKey").getStringValue()) + && LIST_ATTR_VALUE.get(0).asInteger() == valueMap.get(LIST_ATTR_KEY).getListValue() + .getValuesList().get(0).getNumberValue() + && STRUCT_ATTR_INNER_VALUE.equals( valueMap.get(STRUCT_ATTR_KEY).getStructValue().getFieldsMap() .get(STRUCT_ATTR_INNER_KEY).getStringValue()); - } - )) - ).thenReturn(booleanResponse); + }))).thenReturn(booleanResponse); GrpcConnector grpc = mock(GrpcConnector.class); when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - OpenFeatureAPI.getInstance().setProvider(createProvider(grpc)); + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); final MutableContext context = new MutableContext("MY_TARGETING_KEY"); context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE); @@ -382,7 +382,8 @@ void context_is_parsed_and_passed_to_grpc_service() { assertEquals(DEFAULT.toString(), booleanDetails.getReason()); } - // Validates null handling - https://github.com/open-feature/java-sdk-contrib/issues/258 + // Validates null handling - + // https://github.com/open-feature/java-sdk-contrib/issues/258 @Test void null_context_handling() { // given @@ -404,7 +405,7 @@ void null_context_handling() { GrpcConnector grpc = mock(GrpcConnector.class); when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - OpenFeatureAPI.getInstance().setProvider(createProvider(grpc)); + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); // then final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context); @@ -413,7 +414,6 @@ void null_context_handling() { assertEquals(evaluation, expectedVariant); } - @Test void reason_mapped_correctly_if_unknown() { ResolveBooleanResponse badReasonResponse = ResolveBooleanResponse.newBuilder() @@ -430,14 +430,13 @@ void reason_mapped_correctly_if_unknown() { GrpcConnector grpc = mock(GrpcConnector.class); when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - OpenFeatureAPI.getInstance().setProvider(createProvider(grpc)); + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); FlagEvaluationDetails booleanDetails = api.getClient() .getBooleanDetails(FLAG_KEY, false, new MutableContext()); assertEquals(Reason.UNKNOWN.toString(), booleanDetails.getReason()); // reason should be converted to UNKNOWN } - @Test void invalidate_cache() { ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() @@ -470,14 +469,15 @@ void invalidate_cache() { .setReason(STATIC_REASON) .build(); - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); ServiceStub serviceStubMock = mock(ServiceStub.class); when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock); + doNothing().when(serviceStubMock).eventStream(any(), any()); when(serviceStubMock.withDeadline(any(Deadline.class))) .thenReturn(serviceStubMock); when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock.withDeadline(any(Deadline.class))) + when(serviceBlockingStubMock + .withDeadline(any(Deadline.class))) .thenReturn(serviceBlockingStubMock); when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) .thenReturn(serviceBlockingStubMock); @@ -500,20 +500,21 @@ void invalidate_cache() { .thenReturn(serviceStubMock); final Cache cache = new Cache("lru", 5); - grpc = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { - }); - } - FlagdProvider provider = createProvider(grpc); + class NoopInitGrpcConnector extends GrpcConnector { + public NoopInitGrpcConnector(FlagdOptions options, Cache cache, Supplier connectedSupplier, BiConsumer> onResolverConnectionChanged) { + super(options, cache, connectedSupplier, onResolverConnectionChanged); + } - try { - provider.initialize(null); - } catch (Exception e) { - // ignore exception if any + public void initialize() throws Exception {}; + } + + grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys) -> { + }); } - //provider.setState(ProviderState.READY); - OpenFeatureAPI.getInstance().setProvider(provider); + FlagdProvider provider = createProvider(grpc); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); HashMap flagsMap = new HashMap(); HashMap structMap = new HashMap(); @@ -524,8 +525,8 @@ void invalidate_cache() { flagsMap.put(FLAG_KEY_DOUBLE, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); flagsMap.put(FLAG_KEY_OBJECT, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); - structMap.put("flags", com.google.protobuf.Value.newBuilder(). - setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); + structMap.put("flags", com.google.protobuf.Value.newBuilder() + .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); // should cache results FlagEvaluationDetails booleanDetails; @@ -562,7 +563,7 @@ void invalidate_cache() { assertEquals(STATIC_REASON, objectDetails.getReason()); } - private void do_resolvers_cache_responses(String reason, ProviderState eventStreamAlive, Boolean shouldCache) { + private void do_resolvers_cache_responses(String reason, Boolean eventStreamAlive, Boolean shouldCache) { String expectedReason = CACHED_REASON; if (!shouldCache) { expectedReason = reason; @@ -615,8 +616,9 @@ private void do_resolvers_cache_responses(String reason, ProviderState eventStre GrpcConnector grpc = mock(GrpcConnector.class); when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); FlagdProvider provider = createProvider(grpc, () -> eventStreamAlive); - //provider.setState(eventStreamAlive); // caching only available when event stream is alive - OpenFeatureAPI.getInstance().setProvider(provider); + // provider.setState(eventStreamAlive); // caching only available when event + // stream is alive + OpenFeatureAPI.getInstance().setProviderAndWait(provider); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); booleanDetails = api.getClient() @@ -683,7 +685,6 @@ void disabled_cache() { .setReason(STATIC_REASON) .build(); - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); ServiceStub serviceStubMock = mock(ServiceStub.class); when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock); @@ -710,16 +711,27 @@ void disabled_cache() { GrpcConnector grpc; try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) { - mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class))) - .thenReturn(serviceBlockingStubMock); - mockStaticService.when(() -> ServiceGrpc.newStub(any())) - .thenReturn(serviceStubMock); + mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class))) + .thenReturn(serviceBlockingStubMock); + mockStaticService.when(() -> ServiceGrpc.newStub(any())) + .thenReturn(serviceStubMock); + + class NoopInitGrpcConnector extends GrpcConnector { + public NoopInitGrpcConnector(FlagdOptions options, Cache cache, + Supplier connectedSupplier, + BiConsumer> onResolverConnectionChanged) { + super(options, cache, connectedSupplier, onResolverConnectionChanged); + } - grpc = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { - }); + public void initialize() throws Exception { + }; + } + + grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys) -> { + }); } - FlagdProvider provider = createProvider(grpc, cache, () -> ProviderState.READY); + FlagdProvider provider = createProvider(grpc, cache, () -> true); try { provider.initialize(null); @@ -727,7 +739,7 @@ void disabled_cache() { // ignore exception if any } - OpenFeatureAPI.getInstance().setProvider(provider); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); HashMap flagsMap = new HashMap<>(); HashMap structMap = new HashMap<>(); @@ -735,8 +747,8 @@ void disabled_cache() { flagsMap.put("foo", com.google.protobuf.Value.newBuilder().setStringValue("foo") .build()); // assert that a configuration_change event works - structMap.put("flags", com.google.protobuf.Value.newBuilder(). - setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); + structMap.put("flags", com.google.protobuf.Value.newBuilder() + .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); // should not cache results FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); @@ -807,7 +819,7 @@ void contextMerging() throws Exception { } @Test - void initializationAndShutdown() throws Exception{ + void initializationAndShutdown() throws Exception { // given final FlagdProvider provider = new FlagdProvider(); final EvaluationContext ctx = new ImmutableContext(); @@ -828,68 +840,30 @@ void initializationAndShutdown() throws Exception{ provider.shutdown(); provider.shutdown(); - // then + // then verify(resolverMock, times(1)).init(); verify(resolverMock, times(1)).shutdown(); } - @Test - void test_state_on_grpc_resolver_shutdown() throws Exception { - // setup mock provider - final FlagdProvider grpcProvider = Mockito.spy(FlagdProvider.class); - try { - doAnswer(invocation -> { - final Field stateField = FlagdProvider.class.getDeclaredField("state"); - stateField.setAccessible(true); - stateField.set(grpcProvider, ProviderState.READY); - - final Field intializedField = FlagdProvider.class.getDeclaredField("initialized"); - intializedField.setAccessible(true); - intializedField.set(grpcProvider, true); - - return null; - }).when(grpcProvider).initialize(any()); - } catch (Exception e) { - throw new RuntimeException(e); - } - - grpcProvider.initialize(new ImmutableContext()); - assertEquals(ProviderState.READY, grpcProvider.getState()); - grpcProvider.shutdown(); - assertEquals(ProviderState.NOT_READY, grpcProvider.getState()); - } - - @Test - void test_state_on_in_process_resolver_shutdown() throws Exception { - // setup mock in-process provider - FlagdProvider inProcessProvider = createInProcessProvider(); - - inProcessProvider.initialize(new ImmutableContext()); - assertEquals(ProviderState.READY, inProcessProvider.getState()); - inProcessProvider.shutdown(); - assertEquals(ProviderState.NOT_READY, inProcessProvider.getState()); - } - - // test helper // create provider with given grpc connector private FlagdProvider createProvider(GrpcConnector grpc) { - return createProvider(grpc, () -> ProviderState.READY); + return createProvider(grpc, () -> true); } // create provider with given grpc provider and state supplier - private FlagdProvider createProvider(GrpcConnector grpc, Supplier getState) { + private FlagdProvider createProvider(GrpcConnector grpc, Supplier getConnected) { final Cache cache = new Cache("lru", 5); - return createProvider(grpc, cache, getState); + return createProvider(grpc, cache, getConnected); } // create provider with given grpc provider, cache and state supplier - private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier getState) { + private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier getConnected) { final FlagdOptions flagdOptions = FlagdOptions.builder().build(); - final GrpcResolver grpcResolver = - new GrpcResolver(flagdOptions, cache, getState, (providerState,changedFlagKeys) -> { + final GrpcResolver grpcResolver = new GrpcResolver(flagdOptions, cache, getConnected, + (providerState, changedFlagKeys) -> { }); final FlagdProvider provider = new FlagdProvider(); @@ -918,9 +892,8 @@ private FlagdProvider createInProcessProvider() { .build(); final FlagdProvider provider = new FlagdProvider(flagdOptions); final MockStorage mockStorage = new MockStorage(new HashMap(), - new LinkedBlockingQueue(Arrays.asList(new - StorageStateChange(StorageState.OK)))); - + new LinkedBlockingQueue(Arrays.asList(new StorageStateChange(StorageState.OK)))); + try { final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); flagResolver.setAccessible(true); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java index 0345add4d..d07c03259 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java @@ -1,5 +1,7 @@ package dev.openfeature.contrib.providers.flagd.resolver.grpc; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; @@ -22,7 +24,6 @@ import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse; -import dev.openfeature.sdk.ProviderState; class EventStreamObserverTest { @@ -30,7 +31,7 @@ class EventStreamObserverTest { class StateChange { Cache cache; - List states; + List states; EventStreamObserver stream; Runnable reconnect; Object sync; @@ -46,7 +47,7 @@ void setUp() { } @Test - public void Change() { + public void change() { EventStreamResponse resp = mock(EventStreamResponse.class); Struct flagData = mock(Struct.class); when(resp.getType()).thenReturn("configuration_change"); @@ -55,35 +56,35 @@ public void Change() { stream.onNext(resp); // we notify that we are ready assertEquals(1, states.size()); - assertEquals(ProviderState.READY, states.get(0)); + assertTrue(states.get(0)); // we flush the cache verify(cache, atLeast(1)).clear(); } @Test - public void Ready() { + public void ready() { EventStreamResponse resp = mock(EventStreamResponse.class); when(resp.getType()).thenReturn("provider_ready"); stream.onNext(resp); // we notify that we are ready assertEquals(1, states.size()); - assertEquals(ProviderState.READY, states.get(0)); + assertTrue(states.get(0)); // cache was cleaned verify(cache, atLeast(1)).clear(); } @Test - public void Reconnections() { + public void reconnections() { stream.onError(new Throwable("error")); // we flush the cache verify(cache, atLeast(1)).clear(); // we notify the error assertEquals(1, states.size()); - assertEquals(ProviderState.ERROR, states.get(0)); + assertFalse(states.get(0)); } @Test - public void CacheBustingForKnownKeys() { + public void cacheBustingForKnownKeys() { final String key1 = "myKey1"; final String key2 = "myKey2"; @@ -106,7 +107,7 @@ public void CacheBustingForKnownKeys() { stream.onNext(resp); // we notify that the configuration changed assertEquals(1, states.size()); - assertEquals(ProviderState.READY, states.get(0)); + assertTrue(states.get(0)); // we did NOT flush the whole cache verify(cache, atMost(0)).clear(); // we only clean the two keys diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java index 3e360229f..3325341ed 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java @@ -58,7 +58,7 @@ void validate_retry_calls(int retries) throws NoSuchFieldException, IllegalAcces final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); doAnswer(invocation -> null).when(mockStub).eventStream(any(), any()); - final GrpcConnector connector = new GrpcConnector(options, cache, (state,changedFlagKeys) -> { + final GrpcConnector connector = new GrpcConnector(options, cache, () -> true, (state,changedFlagKeys) -> { }); Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub"); @@ -94,20 +94,10 @@ void initialization_succeed_with_connected_status() throws NoSuchFieldException, final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); doAnswer(invocation -> null).when(mockStub).eventStream(any(), any()); - final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { + // pass true in connected lambda + final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys) -> { }); - Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub"); - serviceStubField.setAccessible(true); - serviceStubField.set(connector, mockStub); - - // override default connected state variable - final AtomicBoolean connected = new AtomicBoolean(true); - - Field syncField = GrpcConnector.class.getDeclaredField("connected"); - syncField.setAccessible(true); - syncField.set(connector, connected); - assertDoesNotThrow(connector::initialize); } @@ -118,13 +108,10 @@ void initialization_fail_with_timeout() throws Exception { final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); doAnswer(invocation -> null).when(mockStub).eventStream(any(), any()); - final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { + // pass false in connected lambda + final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, () -> false, (state, changedFlagKeys) -> { }); - Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub"); - serviceStubField.setAccessible(true); - serviceStubField.set(connector, mockStub); - assertThrows(RuntimeException.class, connector::initialize); } @@ -149,7 +136,7 @@ void host_and_port_arg_should_build_tcp_socket() { .forAddress(anyString(), anyInt())).thenReturn(mockChannelBuilder); final FlagdOptions flagdOptions = FlagdOptions.builder().host(host).port(port).tls(false).build(); - new GrpcConnector(flagdOptions, null, null); + new GrpcConnector(flagdOptions, null, null, null); // verify host/port matches mockStaticChannelBuilder.verify(() -> NettyChannelBuilder @@ -180,7 +167,7 @@ void no_args_host_and_port_env_set_should_build_tcp_socket() throws Exception { mockStaticChannelBuilder.when(() -> NettyChannelBuilder .forAddress(anyString(), anyInt())).thenReturn(mockChannelBuilder); - new GrpcConnector(FlagdOptions.builder().build(), null, null); + new GrpcConnector(FlagdOptions.builder().build(), null, null, null); // verify host/port matches & called times(= 1 as we rely on reusable channel) mockStaticChannelBuilder.verify(() -> NettyChannelBuilder. @@ -217,7 +204,7 @@ void path_arg_should_build_domain_socket_with_correct_path() { })) { when(NettyChannelBuilder.forAddress(any(DomainSocketAddress.class))).thenReturn(mockChannelBuilder); - new GrpcConnector(FlagdOptions.builder().socketPath(path).build(), null, null); + new GrpcConnector(FlagdOptions.builder().socketPath(path).build(), null, null, null); // verify path matches mockStaticChannelBuilder.verify(() -> NettyChannelBuilder @@ -260,7 +247,7 @@ void no_args_socket_env_should_build_domain_socket_with_correct_path() throws Ex mockStaticChannelBuilder.when(() -> NettyChannelBuilder .forAddress(any(DomainSocketAddress.class))).thenReturn(mockChannelBuilder); - new GrpcConnector(FlagdOptions.builder().build(), null, null); + new GrpcConnector(FlagdOptions.builder().build(), null, null, null); //verify path matches & called times(= 1 as we rely on reusable channel) mockStaticChannelBuilder.verify(() -> NettyChannelBuilder diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 7452cba6a..f3c2a5773 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -51,388 +51,397 @@ class InProcessResolverTest { - @Test - public void connectorSetup() { - // given - FlagdOptions forGrpcOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS).host("localhost") - .port(8080).build(); - FlagdOptions forOfflineOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .offlineFlagSourcePath("path").build(); - FlagdOptions forCustomConnectorOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .customConnector(new MockConnector(null)).build(); - - // then - assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); - assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); - assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); - } - - @Test - public void eventHandling() throws Throwable { - // given - // note - queues with adequate capacity - final BlockingQueue sender = new LinkedBlockingQueue<>(5); - final BlockingQueue receiver = new LinkedBlockingQueue<>(5); - - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender), - (providerState, changedFlagKeys) -> receiver.offer(providerState)); - - // when - init and emit events - Thread initThread = new Thread(() -> { - try { - inProcessResolver.init(); - } catch (Exception e) { - } - }); - initThread.start(); - if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.EMPTY_LIST), 100, TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); + @Test + public void connectorSetup() { + // given + FlagdOptions forGrpcOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .host("localhost") + .port(8080).build(); + FlagdOptions forOfflineOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .offlineFlagSourcePath("path").build(); + FlagdOptions forCustomConnectorOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .customConnector(new MockConnector(null)).build(); + + // then + assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); + assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); + assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); } - if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); - } - - // then - receive events in order - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - Assertions.assertEquals(ProviderState.READY, receiver.take()); - }); - - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - Assertions.assertEquals(ProviderState.ERROR, receiver.take()); - }); - } - @Test - public void simpleBooleanResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); + @Test + public void eventHandling() throws Throwable { + // given + // note - queues with adequate capacity + final BlockingQueue sender = new LinkedBlockingQueue<>(5); + final BlockingQueue receiver = new LinkedBlockingQueue<>(5); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender), + (connectedState, changedFlagKeys) -> receiver.offer(connectedState)); + + // when - init and emit events + Thread initThread = new Thread(() -> { + try { + inProcessResolver.init(); + } catch (Exception e) { + } + }); + initThread.start(); + if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.EMPTY_LIST), 100, + TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); + } + if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); + } + + // then - receive events in order + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + Assertions.assertTrue(receiver.take()); + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + Assertions.assertFalse(receiver.take()); }); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", false, - new ImmutableContext()); + @Test + public void simpleBooleanResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", + false, + new ImmutableContext()); + + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("on", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("on", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void simpleDoubleResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - @Test - public void simpleDoubleResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("doubleFlag", 0d, + new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("doubleFlag", 0d, - new ImmutableContext()); + // then + assertEquals(3.141d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(3.141d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void fetchIntegerAsDouble() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - @Test - public void fetchIntegerAsDouble() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("doubleFlag", 0, + new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("doubleFlag", 0, - new ImmutableContext()); + // then + assertEquals(3, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(3, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void fetchDoubleAsInt() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - @Test - public void fetchDoubleAsInt() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("integerFlag", 0d, + new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("integerFlag", 0d, - new ImmutableContext()); + // then + assertEquals(1d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(1d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void simpleIntResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - @Test - public void simpleIntResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("integerFlag", 0, + new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("integerFlag", 0, - new ImmutableContext()); + // then + assertEquals(1, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(1, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void simpleObjectResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("objectFlag", OBJECT_FLAG); - @Test - public void simpleObjectResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("objectFlag", OBJECT_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + Map typeDefault = new HashMap<>(); + typeDefault.put("key", "0164"); + typeDefault.put("date", "01.01.1990"); - Map typeDefault = new HashMap<>(); - typeDefault.put("key", "0164"); - typeDefault.put("date", "01.01.1990"); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation("objectFlag", + Value.objectToValue(typeDefault), new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation("objectFlag", - Value.objectToValue(typeDefault), new ImmutableContext()); + // then + Value value = providerEvaluation.getValue(); + Map valueMap = value.asStructure().asMap(); - // then - Value value = providerEvaluation.getValue(); - Map valueMap = value.asStructure().asMap(); + assertEquals("0165", valueMap.get("key").asString()); + assertEquals("01.01.2000", valueMap.get("date").asString()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + assertEquals("typeA", providerEvaluation.getVariant()); + } - assertEquals("0165", valueMap.get("key").asString()); - assertEquals("01.01.2000", valueMap.get("date").asString()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - assertEquals("typeA", providerEvaluation.getVariant()); - } + @Test + public void missingFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); - @Test - public void missingFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + // when/then + ProviderEvaluation missingFlag = inProcessResolver.booleanEvaluation("missingFlag", false, + new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); + } - // when/then - ProviderEvaluation missingFlag = inProcessResolver.booleanEvaluation("missingFlag", false, new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); - } + @Test + public void disabledFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("disabledFlag", DISABLED_FLAG); - @Test - public void disabledFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("disabledFlag", DISABLED_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + // when/then + ProviderEvaluation disabledFlag = inProcessResolver.booleanEvaluation("disabledFlag", false, + new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); + } - // when/then - ProviderEvaluation disabledFlag = inProcessResolver.booleanEvaluation("disabledFlag", false, new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); - } + @Test + public void variantMismatchFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); - @Test - public void variantMismatchFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); }); + } - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); - }); - } + @Test + public void typeMismatchEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", BOOLEAN_FLAG); - @Test - public void typeMismatchEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", BOOLEAN_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); }); + } - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); - }); - } + @Test + public void booleanShorthandEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); - @Test - public void booleanShorthandEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false, + new ImmutableContext()); - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false, - new ImmutableContext()); + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("true", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("true", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + @Test + public void targetingMatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", + "loopAlg", + new MutableContext().add("email", "abc@faas.com")); + + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - @Test - public void targetingMatchedEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + @Test + public void targetingUnmatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", + "loopAlg", + new MutableContext().add("email", "abc@abc.com")); + + // then + assertEquals("loopAlg", providerEvaluation.getValue()); + assertEquals("loop", providerEvaluation.getVariant()); + assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); + } - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + @Test + public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loopAlg", - new MutableContext().add("email", "abc@faas.com")); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loop", + new MutableContext("xyz")); - @Test - public void targetingUnmatchedEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + @Test + public void targetingErrorEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loopAlg", - new MutableContext().add("email", "abc@abc.com")); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); - // then - assertEquals("loopAlg", providerEvaluation.getValue()); - assertEquals("loop", providerEvaluation.getVariant()); - assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); - } + // when/then + assertThrows(ParseError.class, () -> { + inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); + }); + } - @Test - public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); + @Test + public void validateMetadataInEvaluationResult() throws Exception { + // given + final String scope = "appName=myApp"; + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); + + InProcessResolver inProcessResolver = getInProcessResolverWth( + FlagdOptions.builder().selector(scope).build(), + new MockStorage(flagMap)); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", + false, + new ImmutableContext()); + + // then + final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); + assertNotNull(metadata); + assertEquals(scope, metadata.getString("scope")); + } - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + private InProcessResolver getInProcessResolverWth(final FlagdOptions options, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loop", - new MutableContext("xyz")); + final InProcessResolver resolver = new InProcessResolver(options, () -> true, + (providerState, changedFlagKeys) -> { + }); + return injectFlagStore(resolver, storage); + } - // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + private InProcessResolver getInProcessResolverWth(final MockStorage storage, + final BiConsumer> stateConsumer) + throws NoSuchFieldException, IllegalAccessException { - @Test - public void targetingErrorEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); + final InProcessResolver resolver = new InProcessResolver( + FlagdOptions.builder().deadline(1000).build(), () -> true, stateConsumer); + return injectFlagStore(resolver, storage); + } - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys) -> { - }); + // helper to inject flagStore override + private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { - // when/then - assertThrows(ParseError.class, () -> { - inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); - }); - } - - @Test - public void validateMetadataInEvaluationResult() throws Exception { - // given - final String scope = "appName=myApp"; - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); - - InProcessResolver inProcessResolver = getInProcessResolverWth( - FlagdOptions.builder().selector(scope).build(), - new MockStorage(flagMap)); - - // when - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", false, - new ImmutableContext()); - - // then - final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); - assertNotNull(metadata); - assertEquals(scope, metadata.getString("scope")); - } - - private InProcessResolver getInProcessResolverWth(final FlagdOptions options, final MockStorage storage) - throws NoSuchFieldException, IllegalAccessException { - - final InProcessResolver resolver = new InProcessResolver(options, (providerState, changedFlagKeys) -> { - }); - return injectFlagStore(resolver, storage); - } - - private InProcessResolver getInProcessResolverWth(final MockStorage storage, - final BiConsumer> stateConsumer) - throws NoSuchFieldException, IllegalAccessException { - - final InProcessResolver resolver = new InProcessResolver( - FlagdOptions.builder().deadline(1000).build(), stateConsumer); - return injectFlagStore(resolver, storage); - } - - // helper to inject flagStore override - private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) - throws NoSuchFieldException, IllegalAccessException { - - final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); - flagStore.setAccessible(true); - flagStore.set(resolver, storage); - - return resolver; - } + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, storage); + + return resolver; + } } diff --git a/providers/flipt/src/main/java/dev/openfeature/contrib/providers/flipt/FliptProvider.java b/providers/flipt/src/main/java/dev/openfeature/contrib/providers/flipt/FliptProvider.java index 5d69576a6..5977f9689 100644 --- a/providers/flipt/src/main/java/dev/openfeature/contrib/providers/flipt/FliptProvider.java +++ b/providers/flipt/src/main/java/dev/openfeature/contrib/providers/flipt/FliptProvider.java @@ -1,15 +1,18 @@ package dev.openfeature.contrib.providers.flipt; +import static dev.openfeature.sdk.Reason.DEFAULT; +import static dev.openfeature.sdk.Reason.TARGETING_MATCH; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import io.flipt.api.FliptClient; import io.flipt.api.evaluation.models.BooleanEvaluationResponse; import io.flipt.api.evaluation.models.EvaluationRequest; @@ -19,12 +22,6 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import static dev.openfeature.sdk.Reason.DEFAULT; -import static dev.openfeature.sdk.Reason.TARGETING_MATCH; - /** * Provider implementation for Flipt. */ @@ -43,10 +40,6 @@ public class FliptProvider extends EventProvider { @Getter private FliptClient fliptClient; - @Setter(AccessLevel.PROTECTED) - @Getter - private ProviderState state = ProviderState.NOT_READY; - private AtomicBoolean isInitialized = new AtomicBoolean(false); /** @@ -73,8 +66,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { super.initialize(evaluationContext); fliptClient = fliptProviderConfig.getFliptClientBuilder().build(); - state = ProviderState.READY; - log.info("finished initializing provider, state: {}", state); + log.info("finished initializing provider"); } @Override @@ -82,27 +74,8 @@ public Metadata getMetadata() { return () -> NAME; } - @Override - public void emitProviderReady(ProviderEventDetails details) { - super.emitProviderReady(details); - state = ProviderState.READY; - } - - @Override - public void emitProviderError(ProviderEventDetails details) { - super.emitProviderError(details); - state = ProviderState.ERROR; - } - @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); - } - throw new GeneralError(UNKNOWN_ERROR); - } - Map contextMap = ContextTransformer.transform(ctx); EvaluationRequest request = EvaluationRequest.builder().namespaceKey(fliptProviderConfig.getNamespace()) .flagKey(key).entityId(ctx.getTargetingKey()).context(contextMap).build(); @@ -179,12 +152,6 @@ private static Double getDoubleValue(ProviderEvaluation valueProviderEval @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); - } - throw new GeneralError(UNKNOWN_ERROR); - } Map contextMap = ContextTransformer.transform(ctx); EvaluationRequest request = EvaluationRequest.builder().namespaceKey(fliptProviderConfig.getNamespace()) .flagKey(key).entityId(ctx.getTargetingKey()).context(contextMap).build(); @@ -222,6 +189,5 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa public void shutdown() { super.shutdown(); log.info("shutdown"); - state = ProviderState.NOT_READY; } } diff --git a/providers/flipt/src/test/java/dev/openfeature/contrib/providers/flipt/FliptProviderTest.java b/providers/flipt/src/test/java/dev/openfeature/contrib/providers/flipt/FliptProviderTest.java index 3378f840f..6cd62eabc 100644 --- a/providers/flipt/src/test/java/dev/openfeature/contrib/providers/flipt/FliptProviderTest.java +++ b/providers/flipt/src/test/java/dev/openfeature/contrib/providers/flipt/FliptProviderTest.java @@ -173,40 +173,4 @@ void getEvaluationMetadataTest() { evaluationContext); assertEquals(null, nonExistingFlagEvaluation.getFlagMetadata().getBoolean("variant-attachment")); } - - @SneakyThrows - @Test - void shouldThrowIfNotInitialized() { - FliptProvider asyncInitfliptProvider = buildFliptProvider(); - assertEquals(ProviderState.NOT_READY, asyncInitfliptProvider.getState()); - - // ErrorCode.PROVIDER_NOT_READY should be returned when evaluated via the client - assertThrows(ProviderNotReadyError.class, () -> asyncInitfliptProvider - .getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, - () -> asyncInitfliptProvider.getStringEvaluation("fail_not_initialized", "", new ImmutableContext())); - - asyncInitfliptProvider.initialize(null); - assertThrows(GeneralError.class, () -> asyncInitfliptProvider.initialize(null)); - - asyncInitfliptProvider.shutdown(); - } - - @SneakyThrows - @Test - void shouldThrowIfErrorEvent() { - FliptProvider asyncInitfliptProvider = buildFliptProvider(); - asyncInitfliptProvider.initialize(new ImmutableContext()); - - asyncInitfliptProvider.emitProviderError(ProviderEventDetails.builder().build()); - - // ErrorCode.PROVIDER_NOT_READY should be returned when evaluated via the client - assertThrows(GeneralError.class, - () -> asyncInitfliptProvider.getBooleanEvaluation("fail", false, new ImmutableContext())); - assertThrows(GeneralError.class, - () -> asyncInitfliptProvider.getStringEvaluation("fail", "", new ImmutableContext())); - - asyncInitfliptProvider.shutdown(); - } - } \ No newline at end of file diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java index 21c371e55..6a4704c20 100644 --- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java @@ -1,6 +1,13 @@ package dev.openfeature.contrib.providers.gofeatureflag; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.jetbrains.annotations.NotNull; + import com.fasterxml.jackson.core.JsonProcessingException; + import dev.openfeature.contrib.providers.gofeatureflag.bean.ConfigurationChange; import dev.openfeature.contrib.providers.gofeatureflag.controller.CacheController; import dev.openfeature.contrib.providers.gofeatureflag.controller.GoFeatureFlagController; @@ -16,22 +23,14 @@ import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.PublishSubject; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; /** * GoFeatureFlagProvider is the JAVA provider implementation for the feature flag solution GO Feature Flag. @@ -46,7 +45,6 @@ public class GoFeatureFlagProvider extends EventProvider { private final GoFeatureFlagProviderOptions options; private final List hooks = new ArrayList<>(); private DataCollectorHook dataCollectorHook; - private ProviderState state = ProviderState.NOT_READY; private Disposable flagChangeDisposable; private GoFeatureFlagController gofeatureflagController; private CacheController cacheCtrl; @@ -127,9 +125,8 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { this.flagChangeDisposable = this.startCheckFlagConfigurationChangesDaemon(); } - state = ProviderState.READY; super.emitProviderReady(ProviderEventDetails.builder().message("Provider is ready to call the API").build()); - log.info("finishing initializing provider, state: {}", state); + log.info("finishing initializing provider"); } @@ -175,11 +172,6 @@ private Disposable startCheckFlagConfigurationChangesDaemon() { ); } - @Override - public ProviderState getState() { - return state; - } - /** * getEvaluation is the function resolving the flag, it will 1st check in the cache and if it is not available * will call the evaluation endpoint to get the value of the flag. @@ -195,18 +187,6 @@ public ProviderState getState() { private ProviderEvaluation getEvaluation( String key, T defaultValue, EvaluationContext evaluationContext, Class expectedType) { try { - if (!ProviderState.READY.equals(state)) { - if (ProviderState.NOT_READY.equals(state)) { - - /* - should be handled by the SDK framework, ErrorCode.PROVIDER_NOT_READY and default value - should be returned when evaluated via the client. - */ - throw new ProviderNotReadyError("provider not initialized yet"); - } - throw new GeneralError("unknown error, provider state: " + state); - } - if (this.cacheCtrl == null) { return this.gofeatureflagController .evaluateFlag(key, defaultValue, evaluationContext, expectedType) diff --git a/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java b/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java index 625d79606..ed8c0df3f 100644 --- a/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java +++ b/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java @@ -180,33 +180,6 @@ void constructor_options_valid_endpoint() { assertDoesNotThrow(() -> new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder().endpoint("http://localhost:1031").build())); } - @SneakyThrows - @Test - void should_return_not_ready_if_not_initialized() { - GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder().endpoint(this.baseUrl.toString()).timeout(1000).build()) { - @Override - public void initialize(EvaluationContext evaluationContext) throws Exception { - - // make the provider not initialized for this test - Thread.sleep(3000); - } - }; - - /* - ErrorCode.PROVIDER_NOT_READY and default value should be returned when evaluated via the client, - see next step in this test. - */ - assertThrows(ProviderNotReadyError.class, () -> g.getBooleanEvaluation("bool_targeting_match", false, this.evaluationContext)); - - String providerName = "shouldReturnNotReadyIfNotInitialized"; - OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g); - assertThat(OpenFeatureAPI.getInstance().getProvider(providerName).getState()).isEqualTo(ProviderState.NOT_READY); - Client client = OpenFeatureAPI.getInstance().getClient(providerName); - FlagEvaluationDetails booleanFlagEvaluationDetails = client.getBooleanDetails("return_error_when_not_initialized", false, new ImmutableContext("targetingKey")); - assertEquals(ErrorCode.PROVIDER_NOT_READY, booleanFlagEvaluationDetails.getErrorCode()); - assertEquals(Boolean.FALSE, booleanFlagEvaluationDetails.getValue()); - } - @SneakyThrows @Test void client_test() { diff --git a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java index e1baa4099..05cbc8f10 100644 --- a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java +++ b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java @@ -1,32 +1,30 @@ package dev.openfeature.contrib.providers.statsig; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; + +import org.jetbrains.annotations.NotNull; + import com.statsig.sdk.APIFeatureGate; import com.statsig.sdk.DynamicConfig; import com.statsig.sdk.EvaluationReason; import com.statsig.sdk.Layer; import com.statsig.sdk.Statsig; import com.statsig.sdk.StatsigUser; + import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; /** * Provider implementation for Statsig. @@ -36,18 +34,9 @@ public class StatsigProvider extends EventProvider { @Getter private static final String NAME = "Statsig"; - - private static final String PROVIDER_NOT_YET_INITIALIZED = "provider not yet initialized"; - private static final String UNKNOWN_ERROR = "unknown error"; private static final String FEATURE_CONFIG_KEY = "feature_config"; - private final StatsigProviderConfig statsigProviderConfig; - @Getter - private ProviderState state = ProviderState.NOT_READY; - - private final AtomicBoolean isInitialized = new AtomicBoolean(false); - /** * Constructor. * @param statsigProviderConfig StatsigProvider Config @@ -63,19 +52,12 @@ public StatsigProvider(StatsigProviderConfig statsigProviderConfig) { */ @Override public void initialize(EvaluationContext evaluationContext) throws Exception { - boolean initialized = isInitialized.getAndSet(true); - if (initialized && ProviderState.READY.equals(state)) { - log.debug("already initialized"); - return; - } - Future initFuture = Statsig.initializeAsync(statsigProviderConfig.getSdkKey(), statsigProviderConfig.getOptions()); initFuture.get(); statsigProviderConfig.postInit(); - state = ProviderState.READY; - log.info("finished initializing provider, state: {}", state); + log.info("finished initializing provider"); } @Override @@ -87,7 +69,6 @@ public Metadata getMetadata() { @Override @SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification = "reason can be null") public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - verifyEvaluation(); StatsigUser user = ContextTransformer.transform(ctx); Boolean evaluatedValue = defaultValue; Value featureConfigValue = ctx.getValue(FEATURE_CONFIG_KEY); @@ -136,7 +117,6 @@ private boolean assumeFailure(APIFeatureGate featureGate) { @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - verifyEvaluation(); StatsigUser user = ContextTransformer.transform(ctx); FeatureConfig featureConfig = parseFeatureConfig(ctx); String evaluatedValue = defaultValue; @@ -159,7 +139,6 @@ public ProviderEvaluation getStringEvaluation(String key, String default @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - verifyEvaluation(); StatsigUser user = ContextTransformer.transform(ctx); FeatureConfig featureConfig = parseFeatureConfig(ctx); Integer evaluatedValue = defaultValue; @@ -182,7 +161,6 @@ public ProviderEvaluation getIntegerEvaluation(String key, Integer defa @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - verifyEvaluation(); StatsigUser user = ContextTransformer.transform(ctx); FeatureConfig featureConfig = parseFeatureConfig(ctx); Double evaluatedValue = defaultValue; @@ -206,7 +184,6 @@ public ProviderEvaluation getDoubleEvaluation(String key, Double default @SneakyThrows @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { - verifyEvaluation(); StatsigUser user = ContextTransformer.transform(ctx); FeatureConfig featureConfig = parseFeatureConfig(ctx); Value evaluatedValue = defaultValue; @@ -293,27 +270,11 @@ private static FeatureConfig parseFeatureConfig(EvaluationContext ctx) { return new FeatureConfig(type, name); } - private void verifyEvaluation() throws ProviderNotReadyError, GeneralError { - if (!ProviderState.READY.equals(state)) { - - /* - According to spec Requirement 2.4.5: - "The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready." - https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-245 - */ - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); - } - throw new GeneralError(UNKNOWN_ERROR); - } - } - @SneakyThrows @Override public void shutdown() { log.info("shutdown"); Statsig.shutdown(); - state = ProviderState.NOT_READY; } /** diff --git a/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java b/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java index b928255d7..4ac4721b9 100644 --- a/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java +++ b/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java @@ -300,19 +300,7 @@ void getBooleanEvaluationByProperties() { assertEquals(false, statsigProvider.getBooleanEvaluation(PROPERTIES_FLAG_NAME, false, evaluationContext).getValue()); } - - @SneakyThrows - @Test - void shouldThrowIfNotInitialized() { - StatsigProviderConfig statsigProviderConfig = StatsigProviderConfig.builder().sdkKey("test").build(); - StatsigProvider tempstatsigProvider = new StatsigProvider(statsigProviderConfig); - - assertThrows(ProviderNotReadyError.class, ()-> tempstatsigProvider.getBooleanEvaluation( - "fail_not_initialized", false, new ImmutableContext())); - - OpenFeatureAPI.getInstance().setProviderAndWait("tempstatsigProvider", tempstatsigProvider); - } - + @SneakyThrows @Test void contextTransformTest() { diff --git a/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashProvider.java b/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashProvider.java index 380a3979c..436c131df 100644 --- a/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashProvider.java +++ b/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashProvider.java @@ -1,15 +1,16 @@ package dev.openfeature.contrib.providers.unleash; +import static io.getunleash.Variant.DISABLED_VARIANT; + +import java.util.concurrent.atomic.AtomicBoolean; + import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import io.getunleash.DefaultUnleash; import io.getunleash.Unleash; import io.getunleash.UnleashContext; @@ -20,10 +21,6 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.atomic.AtomicBoolean; - -import static io.getunleash.Variant.DISABLED_VARIANT; - /** * Provider implementation for Unleash. */ @@ -45,10 +42,6 @@ public class UnleashProvider extends EventProvider { @Getter private Unleash unleash; - @Setter(AccessLevel.PROTECTED) - @Getter - private ProviderState state = ProviderState.NOT_READY; - private AtomicBoolean isInitialized = new AtomicBoolean(false); /** @@ -78,8 +71,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { unleash = new DefaultUnleash(unleashConfig); // Unleash is per definition ready after it is initialized. - state = ProviderState.READY; - log.info("finished initializing provider, state: {}", state); + log.info("finished initializing provider"); } @Override @@ -87,26 +79,9 @@ public Metadata getMetadata() { return () -> NAME; } - @Override - public void emitProviderReady(ProviderEventDetails details) { - super.emitProviderReady(details); - state = ProviderState.READY; - } - - @Override - public void emitProviderError(ProviderEventDetails details) { - super.emitProviderError(details); - state = ProviderState.ERROR; - } - + @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); - } - throw new GeneralError(UNKNOWN_ERROR); - } UnleashContext context = ctx == null ? UnleashContext.builder().build() : ContextTransformer.transform(ctx); boolean featureBooleanValue = unleash.isEnabled(key, context, defaultValue); return ProviderEvaluation.builder() @@ -172,12 +147,6 @@ private static Double getDoubleValue(ProviderEvaluation valueProviderEval @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); - } - throw new GeneralError(UNKNOWN_ERROR); - } UnleashContext context = ctx == null ? UnleashContext.builder().build() : ContextTransformer.transform(ctx); Variant evaluatedVariant = unleash.getVariant(key, context); String variantName; @@ -209,6 +178,5 @@ public void shutdown() { if (unleash != null) { unleash.shutdown(); } - state = ProviderState.NOT_READY; } } diff --git a/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashSubscriberWrapper.java b/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashSubscriberWrapper.java index 93e9e945a..92c0d5949 100644 --- a/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashSubscriberWrapper.java +++ b/providers/unleash/src/main/java/dev/openfeature/contrib/providers/unleash/UnleashSubscriberWrapper.java @@ -1,8 +1,11 @@ package dev.openfeature.contrib.providers.unleash; +import javax.annotation.Nullable; + import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.ProviderEventDetails; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.getunleash.UnleashException; import io.getunleash.event.ImpressionEvent; import io.getunleash.event.ToggleEvaluated; @@ -17,8 +20,6 @@ import lombok.Generated; import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; - /** * UnleashSubscriber wrapper for emitting event provider events. */ @@ -31,9 +32,11 @@ public class UnleashSubscriberWrapper implements UnleashSubscriber { /** * Constructor. + * * @param unleashSubscriber subscriber - * @param eventProvider events provider for emitting events. + * @param eventProvider events provider for emitting events. */ + @SuppressFBWarnings(value = { "EI_EXPOSE_REP" }) public UnleashSubscriberWrapper(@Nullable UnleashSubscriber unleashSubscriber, EventProvider eventProvider) { this.unleashSubscriber = unleashSubscriber; this.eventProvider = eventProvider; diff --git a/providers/unleash/src/test/java/dev/openfeature/contrib/providers/unleash/UnleashProviderTest.java b/providers/unleash/src/test/java/dev/openfeature/contrib/providers/unleash/UnleashProviderTest.java index 915b38e56..ff2d68c2f 100644 --- a/providers/unleash/src/test/java/dev/openfeature/contrib/providers/unleash/UnleashProviderTest.java +++ b/providers/unleash/src/test/java/dev/openfeature/contrib/providers/unleash/UnleashProviderTest.java @@ -1,18 +1,38 @@ package dev.openfeature.contrib.providers.unleash; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; + import dev.openfeature.sdk.Client; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import io.getunleash.UnleashContext; import io.getunleash.UnleashException; import io.getunleash.event.ToggleEvaluated; @@ -21,29 +41,6 @@ import io.getunleash.repository.FeatureToggleResponse; import io.getunleash.util.UnleashConfig; import lombok.SneakyThrows; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.net.URI; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.any; -import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; /** * UnleashProvider test, based on APIs mocking. @@ -206,37 +203,6 @@ void getEvaluationMetadataTest() { assertEquals(false, nonExistingFlagEvaluation.getFlagMetadata().getBoolean("enabled")); } - @SneakyThrows - @Test - void shouldThrowIfNotInitialized() { - UnleashProvider asyncInitUnleashProvider = buildUnleashProvider(false, "http://fakeAPI", new TestSubscriber()); - assertEquals(ProviderState.NOT_READY, asyncInitUnleashProvider.getState()); - - // ErrorCode.PROVIDER_NOT_READY should be returned when evaluated via the client - assertThrows(ProviderNotReadyError.class, ()-> asyncInitUnleashProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> asyncInitUnleashProvider.getStringEvaluation("fail_not_initialized", "", new ImmutableContext())); - - asyncInitUnleashProvider.initialize(null); - assertThrows(GeneralError.class, ()-> asyncInitUnleashProvider.initialize(null)); - - asyncInitUnleashProvider.shutdown(); - } - - @SneakyThrows - @Test - void shouldThrowIfErrorEvent() { - UnleashProvider asyncInitUnleashProvider = buildUnleashProvider(false, "http://fakeAPI", null); - asyncInitUnleashProvider.initialize(new ImmutableContext()); - - asyncInitUnleashProvider.emitProviderError(ProviderEventDetails.builder().build()); - - // ErrorCode.PROVIDER_NOT_READY should be returned when evaluated via the client - assertThrows(GeneralError.class, ()-> asyncInitUnleashProvider.getBooleanEvaluation("fail", false, new ImmutableContext())); - assertThrows(GeneralError.class, ()-> asyncInitUnleashProvider.getStringEvaluation("fail", "", new ImmutableContext())); - - asyncInitUnleashProvider.shutdown(); - } - @SneakyThrows @Test void contextTransformTest() { diff --git a/tools/junit-openfeature/pom.xml b/tools/junit-openfeature/pom.xml index e6c127559..707395c6c 100644 --- a/tools/junit-openfeature/pom.xml +++ b/tools/junit-openfeature/pom.xml @@ -26,13 +26,6 @@ - - dev.openfeature - sdk - [1.4,2.0) - provided - - org.apache.commons commons-lang3