diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 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 bed6c8380..24f614656 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 @@ -80,7 +80,7 @@ public synchronized void initialize(EvaluationContext evaluationContext) throws @Override public synchronized void shutdown() { - if (!initialized) { + if (!this.initialized) { return; } @@ -162,6 +162,12 @@ private void handleStateTransition(ProviderState oldState, ProviderState newStat 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; + } // configuration changed if (ProviderState.READY.equals(oldState) && ProviderState.READY.equals(newState)) { log.debug("Configuration changed"); 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 1f89e260e..290ba3669 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 @@ -100,6 +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); } } 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 358b7cd6c..e62a9fd49 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 @@ -100,6 +100,7 @@ public void init() throws Exception { public void shutdown() throws InterruptedException { flagStore.shutdown(); this.connected.set(false); + stateConsumer.accept(ProviderState.NOT_READY); } /** 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 3235b0d50..fede10600 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 @@ -8,6 +8,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; @@ -16,15 +17,18 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import java.util.concurrent.LinkedBlockingQueue; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; +import org.mockito.Mockito; import com.google.protobuf.Struct; @@ -32,6 +36,10 @@ import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcConnector; 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.MockStorage; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanRequest; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanResponse; @@ -824,6 +832,43 @@ void initializationAndShutdown() throws Exception{ 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 @@ -863,4 +908,29 @@ private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier

(), new LinkedBlockingQueue(Arrays.asList(StorageState.OK))); + + try { + final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); + flagResolver.setAccessible(true); + final Resolver resolver = (Resolver) flagResolver.get(provider); + + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, mockStorage); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + return provider; + } + } 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 2219b02b9..04c043bf4 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 @@ -8,17 +8,17 @@ import java.util.Map; import java.util.concurrent.BlockingQueue; -class MockStorage implements Storage { +public class MockStorage implements Storage { private final Map mockFlags; private final BlockingQueue mockQueue; - MockStorage(Map mockFlags, BlockingQueue mockQueue) { + public MockStorage(Map mockFlags, BlockingQueue mockQueue) { this.mockFlags = mockFlags; this.mockQueue = mockQueue; } - MockStorage(Map flagMap) { + public MockStorage(Map flagMap) { this.mockFlags = flagMap; this.mockQueue = null; }