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 24f614656..c792a5ba1 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 @@ -4,6 +4,7 @@ import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.FeatureProvider; @@ -14,6 +15,7 @@ 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; @@ -142,7 +144,7 @@ private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) { return clientCallCtx; } - private void setState(ProviderState newState) { + private void setState(ProviderState newState, List changedFlagsKeys) { ProviderState oldState; Lock l = this.lock.writeLock(); try { @@ -152,17 +154,17 @@ private void setState(ProviderState newState) { } finally { l.unlock(); } - this.handleStateTransition(oldState, newState); + this.handleStateTransition(oldState, newState, changedFlagsKeys); } - private void handleStateTransition(ProviderState oldState, ProviderState newState) { + 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 + // 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"); @@ -172,6 +174,9 @@ private void handleStateTransition(ProviderState oldState, ProviderState newStat if (ProviderState.READY.equals(oldState) && ProviderState.READY.equals(newState)) { log.debug("Configuration changed"); ProviderEventDetails details = ProviderEventDetails.builder().message("configuration changed").build(); + if (!changedFlagKeys.isEmpty()) { + details.setFlagsChanged(changedFlagKeys); + } this.emitProviderConfigurationChanged(details); return; } 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 290ba3669..b7f6066da 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 @@ -1,10 +1,10 @@ package dev.openfeature.contrib.providers.flagd.resolver.grpc; +import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - +import java.util.function.BiConsumer; 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; @@ -37,7 +37,7 @@ public class GrpcConnector { private final long deadline; private final Cache cache; - private final Consumer stateConsumer; + private final BiConsumer> stateConsumer; private int eventStreamAttempt = 1; private int eventStreamRetryBackoff; @@ -52,7 +52,7 @@ 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, Consumer stateConsumer) { + public GrpcConnector(final FlagdOptions options, final Cache cache, BiConsumer> stateConsumer) { this.channel = ChannelBuilder.nettyChannel(options); this.serviceStub = ServiceGrpc.newStub(channel); this.serviceBlockingStub = ServiceGrpc.newBlockingStub(channel); @@ -100,7 +100,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); + this.stateConsumer.accept(ProviderState.NOT_READY, null); } } @@ -162,6 +162,6 @@ private void grpcStateConsumer(final ProviderState state) { } // chain to initiator - this.stateConsumer.accept(state); + this.stateConsumer.accept(state, null); } } 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 9879d6c55..a8cd77ec9 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 @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -62,7 +63,7 @@ public final class GrpcResolver implements Resolver { * @param stateConsumer lambda to communicate back the state. */ public GrpcResolver(final FlagdOptions options, final Cache cache, final Supplier stateSupplier, - final Consumer stateConsumer) { + final BiConsumer> stateConsumer) { this.cache = cache; this.stateSupplier = stateSupplier; 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 e3a525e20..96a3a6022 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 @@ -23,8 +23,13 @@ import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING; @@ -36,7 +41,7 @@ @Slf4j public class InProcessResolver implements Resolver { private final Storage flagStore; - private final Consumer stateConsumer; + private final BiConsumer> stateConsumer; private final Operator operator; private final long deadline; private final ImmutableMetadata metadata; @@ -45,7 +50,7 @@ public class InProcessResolver implements Resolver { /** * Initialize an in-process resolver. */ - public InProcessResolver(FlagdOptions options, Consumer stateConsumer) { + public InProcessResolver(FlagdOptions options, BiConsumer> stateConsumer) { this.flagStore = new FlagStore(getConnector(options)); this.deadline = options.getDeadline(); this.stateConsumer = stateConsumer; @@ -67,11 +72,12 @@ public void init() throws Exception { final StorageState storageState = flagStore.getStateQueue().take(); switch (storageState) { case OK: - stateConsumer.accept(ProviderState.READY); + stateConsumer.accept(ProviderState.READY, + flagStore.getChangedFlags().keySet().stream().collect(Collectors.toList())); this.connected.set(true); break; case ERROR: - stateConsumer.accept(ProviderState.ERROR); + stateConsumer.accept(ProviderState.ERROR,null); this.connected.set(false); break; case STALE: @@ -100,14 +106,14 @@ public void init() throws Exception { public void shutdown() throws InterruptedException { flagStore.shutdown(); this.connected.set(false); - stateConsumer.accept(ProviderState.NOT_READY); + stateConsumer.accept(ProviderState.NOT_READY,null); } /** * Resolve a boolean flag. */ public ProviderEvaluation booleanEvaluation(String key, Boolean defaultValue, - EvaluationContext ctx) { + EvaluationContext ctx) { return resolve(Boolean.class, key, ctx); } @@ -115,7 +121,7 @@ public ProviderEvaluation booleanEvaluation(String key, Boolean default * Resolve a string flag. */ public ProviderEvaluation stringEvaluation(String key, String defaultValue, - EvaluationContext ctx) { + EvaluationContext ctx) { return resolve(String.class, key, ctx); } @@ -123,7 +129,7 @@ public ProviderEvaluation stringEvaluation(String key, String defaultVal * Resolve a double flag. */ public ProviderEvaluation doubleEvaluation(String key, Double defaultValue, - EvaluationContext ctx) { + EvaluationContext ctx) { return resolve(Double.class, key, ctx); } @@ -131,7 +137,7 @@ public ProviderEvaluation doubleEvaluation(String key, Double defaultVal * Resolve an integer flag. */ public ProviderEvaluation integerEvaluation(String key, Integer defaultValue, - EvaluationContext ctx) { + EvaluationContext ctx) { return resolve(Integer.class, key, ctx); } @@ -161,7 +167,7 @@ static Connector getConnector(final FlagdOptions options) { } private ProviderEvaluation resolve(Class type, String key, - EvaluationContext ctx) { + EvaluationContext ctx) { final FeatureFlag flag = flagStore.getFlag(key); // missing flag diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java index ee642cca9..0be76a972 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java @@ -5,6 +5,7 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; @@ -31,6 +32,9 @@ public class FlagStore implements Storage { private final BlockingQueue stateBlockingQueue = new LinkedBlockingQueue<>(1); private final Map flags = new HashMap<>(); + @Getter + private final Map changedFlags = new HashMap<>(); + private final Connector connector; private final boolean throwIfInvalid; @@ -103,6 +107,7 @@ private void streamerListener(final Connector connector) throws InterruptedExcep Map flagMap = FlagParser.parseString(take.getData(), throwIfInvalid); writeLock.lock(); try { + updateChangedFlags(flagMap); flags.clear(); flags.putAll(flagMap); } finally { @@ -132,4 +137,26 @@ private void streamerListener(final Connector connector) throws InterruptedExcep log.info("Shutting down store stream listener"); } + private void updateChangedFlags(Map newFlags) { + changedFlags.clear(); + Map addedFeatureFlags = new HashMap<>(); + Map removedFeatureFlags = new HashMap<>(); + Map updatedFeatureFlags = new HashMap<>(); + newFlags.forEach((key, value) -> { + if (!flags.containsKey(key)) { + addedFeatureFlags.put(key, value); + } else if (flags.containsKey(key) && !value.equals(flags.get(key))) { + updatedFeatureFlags.put(key, value); + } + }); + flags.forEach((key,value) -> { + if(!newFlags.containsKey(key)) { + removedFeatureFlags.put(key, value); + } + }); + changedFlags.putAll(addedFeatureFlags); + changedFlags.putAll(removedFeatureFlags); + changedFlags.putAll(updatedFeatureFlags); + } + } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java index 32337094e..021312111 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java @@ -2,6 +2,7 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import java.util.Map; import java.util.concurrent.BlockingQueue; /** @@ -14,5 +15,7 @@ public interface Storage { FeatureFlag getFlag(final String key); + Map getChangedFlags(); + BlockingQueue getStateQueue(); } 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 fede10600..a615efba1 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 @@ -499,7 +499,7 @@ void invalidate_cache() { .thenReturn(serviceStubMock); final Cache cache = new Cache("lru", 5); - grpc = new GrpcConnector(FlagdOptions.builder().build(), cache, state -> { + grpc = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { }); } @@ -714,7 +714,7 @@ void disabled_cache() { mockStaticService.when(() -> ServiceGrpc.newStub(any())) .thenReturn(serviceStubMock); - grpc = new GrpcConnector(FlagdOptions.builder().build(), cache, state -> { + grpc = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { }); } @@ -888,7 +888,7 @@ private FlagdProvider createProvider(GrpcConnector grpc, Supplier private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier getState) { final FlagdOptions flagdOptions = FlagdOptions.builder().build(); final GrpcResolver grpcResolver = - new GrpcResolver(flagdOptions, cache, getState, (providerState) -> { + new GrpcResolver(flagdOptions, cache, getState, (providerState,changedFlagKeys) -> { }); final FlagdProvider provider = new FlagdProvider(); 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 992691125..d81797747 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 @@ -57,7 +57,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) -> { + final GrpcConnector connector = new GrpcConnector(options, cache, (state,changedFlagKeys) -> { }); Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub"); @@ -93,7 +93,7 @@ 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) -> { + final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { }); Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub"); @@ -117,7 +117,7 @@ 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) -> { + final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, (state,changedFlagKeys) -> { }); Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub"); 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 d6a063c16..93e44c7b0 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 @@ -23,11 +23,12 @@ import java.lang.reflect.Field; import java.time.Duration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.BOOLEAN_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.DISABLED_FLAG; @@ -51,7 +52,7 @@ class InProcessResolverTest { public void connectorSetup(){ // given FlagdOptions forGrpcOptions = - FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS).host("localhost").port(8080).build(); + 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 = @@ -71,9 +72,7 @@ public void eventHandling() throws Throwable { final BlockingQueue receiver = new LinkedBlockingQueue<>(5); InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender), - providerState -> { - receiver.offer(providerState); - }); + (providerState, changedFlagKeys) -> receiver.offer(providerState)); // when - init and emit events Thread initThread = new Thread(() -> { @@ -106,8 +105,9 @@ public void simpleBooleanResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("booleanFlag", BOOLEAN_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { - }); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); // when ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", false, @@ -125,8 +125,9 @@ public void simpleDoubleResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("doubleFlag", DOUBLE_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { - }); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); // when ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("doubleFlag", 0d, @@ -144,8 +145,9 @@ public void fetchIntegerAsDouble() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("doubleFlag", DOUBLE_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { - }); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { + }); // when ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("doubleFlag", 0, @@ -163,7 +165,8 @@ public void fetchDoubleAsInt() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("integerFlag", INT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState, changedFlagKeys) -> { }); // when @@ -182,7 +185,8 @@ public void simpleIntResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("integerFlag", INT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when @@ -201,7 +205,8 @@ public void simpleObjectResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("objectFlag", OBJECT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); Map typeDefault = new HashMap<>(); @@ -227,7 +232,8 @@ public void missingFlag() throws Exception { // given final Map flagMap = new HashMap<>(); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when/then @@ -243,7 +249,8 @@ public void disabledFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("disabledFlag", DISABLED_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when/then @@ -258,7 +265,8 @@ public void variantMismatchFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when/then @@ -273,7 +281,8 @@ public void typeMismatchEvaluation() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", BOOLEAN_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when/then @@ -288,7 +297,8 @@ public void booleanShorthandEvaluation() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false, @@ -306,7 +316,8 @@ public void targetingMatchedEvaluationFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when @@ -325,7 +336,8 @@ public void targetingUnmatchedEvaluationFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when @@ -344,7 +356,8 @@ public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalA final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when @@ -363,7 +376,8 @@ public void targetingErrorEvaluationFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), providerState -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (providerState,changedFlagKeys) -> { }); // when/then @@ -396,13 +410,14 @@ public void validateMetadataInEvaluationResult() throws Exception { private InProcessResolver getInProcessResolverWth(final FlagdOptions options, final MockStorage storage) throws NoSuchFieldException, IllegalAccessException { - final InProcessResolver resolver = new InProcessResolver(options, providerState -> {}); + final InProcessResolver resolver = new InProcessResolver(options, (providerState, changedFlagKeys) -> { + }); return injectFlagStore(resolver, storage); } private InProcessResolver getInProcessResolverWth(final MockStorage storage, - final Consumer stateConsumer) + final BiConsumer> stateConsumer) throws NoSuchFieldException, IllegalAccessException { final InProcessResolver resolver = new InProcessResolver( diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java index 04c043bf4..2a92c642d 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java @@ -35,6 +35,11 @@ public FeatureFlag getFlag(String key) { return mockFlags.get(key); } + @Override + public Map getChangedFlags() { + return mockFlags; + } + @Nullable public BlockingQueue getStateQueue() { return mockQueue; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java index 7fd7ab056..09605a70b 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java @@ -1,19 +1,20 @@ package dev.openfeature.contrib.providers.flagd.resolver.process.storage; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FlagParser; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType; +import org.junit.Assert; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.time.Duration; +import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG; -import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG; -import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE; -import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.getFlagsFromResource; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.*; +import static org.junit.jupiter.api.Assertions.*; class FlagStoreTest { @@ -71,4 +72,30 @@ public void connectorHandling() throws Exception { }); } + @Test + public void changedFlags() throws Exception { + final BlockingQueue payload = new LinkedBlockingQueue<>(); + FlagStore store = new FlagStore(new MockConnector(payload), true); + store.init(); + payload.offer(new StreamPayload(StreamPayloadType.DATA, getFlagsFromResource(VALID_SIMPLE))); + store.getStateQueue().take(); + Map changedFlags = store.getChangedFlags(); + + // flags changed for first time + Assert.assertEquals(Mockito.eq(FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE), true)), + Mockito.eq(changedFlags)); + + payload.offer(new StreamPayload(StreamPayloadType.DATA, getFlagsFromResource(VALID_LONG))); + store.getStateQueue().take(); + changedFlags = store.getChangedFlags(); + Map expectedChangedFlags = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE),true); + expectedChangedFlags.remove("myBoolFlag"); + + // flags changed from initial VALID_SIMPLE flag + + Assert.assertEquals(Mockito.eq(expectedChangedFlags),Mockito.eq(changedFlags)); + + + } + }