From 48cd0f9c5a4be9eafdd587e95eb7ba794d98d91a Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 2 Dec 2025 16:47:11 +0100 Subject: [PATCH 01/16] add vmlens Signed-off-by: christian.lutnik --- providers/flagd/pom.xml | 27 ++++++ .../providers/flagd/FlagdProviderCT.java | 86 ++++++++++++++++++ .../providers/flagd/FlagdProviderTest.java | 76 ++-------------- .../providers/flagd/FlagdTestUtils.java | 88 +++++++++++++++++++ 4 files changed, 208 insertions(+), 69 deletions(-) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml index 47073039a..6e7625814 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -19,6 +19,7 @@ 1.79.0 3.25.6 + 1.2.22 flagd @@ -168,6 +169,13 @@ 2.0.17 test + + + com.vmlens + api + ${com.vmlens.version} + test + @@ -281,6 +289,25 @@ dev.openfeature.contrib.- + + com.vmlens + vmlens-maven-plugin + ${com.vmlens.version} + + + test + + test + + + + **/*CT.java + + true + + + + diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java new file mode 100644 index 000000000..b02d340e3 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java @@ -0,0 +1,86 @@ +package dev.openfeature.contrib.providers.flagd; + +import com.vmlens.api.AllInterleavings; +import com.vmlens.api.Runner; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Value; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FlagdProviderCT { + private FlagdProvider provider; + + @BeforeEach + void setup() throws Exception { + provider = FlagdTestUtils.createInProcessProvider( + Map.of( + "flag", + new FeatureFlag( + "ENABLED", + "default", + Map.of("default", "a", "other", "b"), + "{\n" + + " \"if\": [\n" + + " {\n" + + " \"ends_with\": [\n" + + " {\n" + + " \"var\": \"email\"\n" + + " },\n" + + " \"@ingen.com\"\n" + + " ]\n" + + " },\n" + + " \"default\",\n" + + " \"other\"\n" + + " ]\n" + + " }", + null + ) + ) + ); + provider.initialize(ImmutableContext.EMPTY); + } +/* + @Test + void concurrentFlagEvaluationsWork() { + var invocationContext = ImmutableContext.EMPTY; + + try (var interleavings = new AllInterleavings("Concurrent Flag evaluations")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> assertEquals("a", + provider.getStringEvaluation("flag", "z", invocationContext).getValue()), + () -> assertEquals("a", + provider.getStringEvaluation("flag", "z", invocationContext).getValue()) + ); + } + } + }*/ + + @Test + void flagEvaluationsWhileSettingContextWork() { + var invocationContext = ImmutableContext.EMPTY; + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + var client = OpenFeatureAPI.getInstance().getClient(); + + var context = new ImmutableContext(Map.of("email", new Value("someone@ingen.com"))); + + try (var interleavings = new AllInterleavings("Concurrently setting client context and evaluating a Flag")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> assertTrue(List.of("a", "b") + .contains(provider.getStringEvaluation("flag", "z", invocationContext).getValue())), + () -> client.setEvaluationContext(context) + ); + } + } + } +} 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 ad38fe15d..0fcc47a39 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 @@ -156,7 +156,7 @@ void resolvers_call_grpc_service_and_return_details() { .thenReturn(objectResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertTrue(booleanDetails.getValue()); @@ -234,7 +234,7 @@ void zero_value() { ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertEquals(false, booleanDetails.getValue()); @@ -298,7 +298,7 @@ void test_metadata_from_grpc_response() { .thenReturn(booleanResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); // when FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); @@ -380,7 +380,7 @@ void context_is_parsed_and_passed_to_grpc_service() { .thenReturn(booleanResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); final MutableContext context = new MutableContext("MY_TARGETING_KEY"); context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE); @@ -419,7 +419,7 @@ void null_context_handling() { .build()); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); // then final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context); @@ -443,7 +443,7 @@ void reason_mapped_correctly_if_unknown() { .thenReturn(badReasonResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, new MutableContext()); @@ -502,7 +502,7 @@ private void doResolversCacheResponses(String reason, Boolean eventStreamAlive, .thenReturn(objectResponse); ChannelConnector grpc = mock(ChannelConnector.class); - FlagdProvider provider = createProvider(grpc, serviceBlockingStubMock); + FlagdProvider provider = FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock); // provider.setState(eventStreamAlive); // caching only available when event // stream is alive @@ -711,66 +711,4 @@ void initAfterFatalPropagatesErrorEvent() { Assertions.assertEquals(ErrorCode.PROVIDER_FATAL, error.getErrorCode()); } } - - // test helper - // create provider with given grpc provider and state supplier - private FlagdProvider createProvider(ChannelConnector connector, ServiceBlockingStub mockBlockingStub) { - final Cache cache = new Cache("lru", 5); - final ServiceStub mockStub = mock(ServiceStub.class); - - return createProvider(connector, cache, mockStub, mockBlockingStub); - } - - // create provider with given grpc provider, cache and state supplier - private FlagdProvider createProvider( - ChannelConnector connector, Cache cache, ServiceStub mockStub, ServiceBlockingStub mockBlockingStub) { - final FlagdOptions flagdOptions = FlagdOptions.builder().build(); - final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (event, details, metadata) -> {}); - - try { - Field resolver = RpcResolver.class.getDeclaredField("connector"); - resolver.setAccessible(true); - resolver.set(grpcResolver, connector); - - Field stub = RpcResolver.class.getDeclaredField("stub"); - stub.setAccessible(true); - stub.set(grpcResolver, mockStub); - - Field blockingStub = RpcResolver.class.getDeclaredField("blockingStub"); - blockingStub.setAccessible(true); - blockingStub.set(grpcResolver, mockBlockingStub); - - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - final FlagdProvider provider = new FlagdProvider(grpcResolver, true); - return provider; - } - - // Create an in process provider - private FlagdProvider createInProcessProvider() { - - final FlagdOptions flagdOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .deadline(1000) - .build(); - final FlagdProvider provider = new FlagdProvider(flagdOptions); - final MockStorage mockStorage = new MockStorage( - new HashMap(), - new LinkedBlockingQueue(Arrays.asList(new StorageStateChange(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/FlagdTestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java new file mode 100644 index 000000000..fdcb0adbc --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java @@ -0,0 +1,88 @@ +package dev.openfeature.contrib.providers.flagd; + +import dev.openfeature.contrib.providers.flagd.resolver.Resolver; +import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelConnector; +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.contrib.providers.flagd.resolver.process.storage.StorageStateChange; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.RpcResolver; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.Cache; +import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.mockito.Mockito.mock; + +class FlagdTestUtils { + // test helper + // create provider with given grpc provider and state supplier + static FlagdProvider createProvider(ChannelConnector connector, ServiceGrpc.ServiceBlockingStub mockBlockingStub) { + final Cache cache = new Cache("lru", 5); + final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); + + return createProvider(connector, cache, mockStub, mockBlockingStub); + } + + // create provider with given grpc provider, cache and state supplier + static FlagdProvider createProvider( + ChannelConnector connector, Cache cache, ServiceGrpc.ServiceStub mockStub, + ServiceGrpc.ServiceBlockingStub mockBlockingStub) { + final FlagdOptions flagdOptions = FlagdOptions.builder().build(); + final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (connectionEvent) -> {}); + + try { + Field resolver = RpcResolver.class.getDeclaredField("connector"); + resolver.setAccessible(true); + resolver.set(grpcResolver, connector); + + Field stub = RpcResolver.class.getDeclaredField("stub"); + stub.setAccessible(true); + stub.set(grpcResolver, mockStub); + + Field blockingStub = RpcResolver.class.getDeclaredField("blockingStub"); + blockingStub.setAccessible(true); + blockingStub.set(grpcResolver, mockBlockingStub); + + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + final FlagdProvider provider = new FlagdProvider(grpcResolver, true); + return provider; + } + + static FlagdProvider createInProcessProvider(Map mockFlags) { + final FlagdOptions flagdOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .offlineFlagSourcePath("") // this is new + .deadline(1000) + .build(); + final FlagdProvider provider = new FlagdProvider(flagdOptions); + final MockStorage mockStorage = new MockStorage( + mockFlags, + new LinkedBlockingQueue<>(Arrays.asList(new StorageStateChange(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; + } + + static FlagdProvider createInProcessProvider() { + return createInProcessProvider(Collections.emptyMap()); + } +} From f4094df2616fad33aaafc2247bc5f6cb4bf2f69c Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 4 Dec 2025 10:35:09 +0100 Subject: [PATCH 02/16] add vmlens Signed-off-by: christian.lutnik --- .../contrib/providers/flagd/FlagdProviderCT.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java index b02d340e3..dcf447137 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java @@ -1,19 +1,18 @@ package dev.openfeature.contrib.providers.flagd; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.vmlens.api.AllInterleavings; import com.vmlens.api.Runner; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.Value; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import java.util.Collections; import java.util.List; import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; class FlagdProviderCT { private FlagdProvider provider; @@ -47,7 +46,7 @@ void setup() throws Exception { ); provider.initialize(ImmutableContext.EMPTY); } -/* + @Test void concurrentFlagEvaluationsWork() { var invocationContext = ImmutableContext.EMPTY; @@ -62,7 +61,7 @@ void concurrentFlagEvaluationsWork() { ); } } - }*/ + } @Test void flagEvaluationsWhileSettingContextWork() { From eb5cc44ef9da0ad077ec467778ddea62ea3a0ad9 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 10 Dec 2025 11:57:34 +0100 Subject: [PATCH 03/16] minor improvements Signed-off-by: christian.lutnik --- .../resolver/process/InProcessResolver.java | 12 +++---- .../resolver/process/model/FeatureFlag.java | 5 +-- .../providers/flagd/FlagdProviderCT.java | 35 +++++++++++++++---- 3 files changed, 37 insertions(+), 15 deletions(-) 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 ba69b3ad7..d6bf31318 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 @@ -216,7 +216,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " not found") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -225,7 +225,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " is disabled") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -260,7 +260,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC .reason(Reason.ERROR.toString()) .errorCode(ErrorCode.FLAG_NOT_FOUND) .errorMessage("Flag '" + key + "' has no default variant defined, will use code default") - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -285,11 +285,11 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC .value((T) value) .variant(resolvedVariant) .reason(reason) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } - private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) { + private static ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult, String scope) { ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); for (Map.Entry entry : storageQueryResult.getFlagSetMetadata().entrySet()) { @@ -310,7 +310,7 @@ private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) return metadataBuilder.build(); } - private void addEntryToMetadataBuilder( + private static void addEntryToMetadataBuilder( ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder, String key, Object value) { if (value instanceof Number) { if (value instanceof Long) { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index 2eaf2ed87..e8aaedafe 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import lombok.EqualsAndHashCode; @@ -39,7 +40,7 @@ public FeatureFlag( this.variants = variants; this.targeting = targeting; if (metadata == null) { - this.metadata = new HashMap<>(); + this.metadata = Collections.emptyMap(); } else { this.metadata = metadata; } @@ -51,7 +52,7 @@ public FeatureFlag(String state, String defaultVariant, Map vari this.defaultVariant = defaultVariant; this.variants = variants; this.targeting = targeting; - this.metadata = new HashMap<>(); + this.metadata = Collections.emptyMap(); } /** Get targeting rule of the flag. */ diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java index dcf447137..e323f79e2 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java @@ -24,8 +24,8 @@ void setup() throws Exception { "flag", new FeatureFlag( "ENABLED", - "default", - Map.of("default", "a", "other", "b"), + "a", + Map.of("a", "a", "b", "b", "c", "c"), "{\n" + " \"if\": [\n" + " {\n" @@ -36,8 +36,8 @@ void setup() throws Exception { + " \"@ingen.com\"\n" + " ]\n" + " },\n" - + " \"default\",\n" - + " \"other\"\n" + + " \"b\",\n" + + " \"c\"\n" + " ]\n" + " }", null @@ -54,9 +54,9 @@ void concurrentFlagEvaluationsWork() { try (var interleavings = new AllInterleavings("Concurrent Flag evaluations")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> assertEquals("a", + () -> assertEquals("c", provider.getStringEvaluation("flag", "z", invocationContext).getValue()), - () -> assertEquals("a", + () -> assertEquals("c", provider.getStringEvaluation("flag", "z", invocationContext).getValue()) ); } @@ -75,11 +75,32 @@ void flagEvaluationsWhileSettingContextWork() { try (var interleavings = new AllInterleavings("Concurrently setting client context and evaluating a Flag")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> assertTrue(List.of("a", "b") + () -> assertTrue(List.of("b", "c") .contains(provider.getStringEvaluation("flag", "z", invocationContext).getValue())), () -> client.setEvaluationContext(context) ); } } } + + @Test + void settingDifferentContextsWorks() { + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + var client = OpenFeatureAPI.getInstance().getClient(); + + var clientContext = new ImmutableContext(Map.of("email", new Value("someone@ingen.com"))); + var apiContext = new ImmutableContext(Map.of("email", new Value("someone.else@test.com"))); + + try (var interleavings = new AllInterleavings("Concurrently setting client and api context")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> client.setEvaluationContext(clientContext), + () -> OpenFeatureAPI.getInstance().setEvaluationContext(apiContext), + () -> assertTrue(List.of("b", "c") + .contains(provider.getStringEvaluation("flag", "z", ImmutableContext.EMPTY).getValue())) + ); + } + } + } } From a25a221e5070bfe2d0a138b6b26a9bf8e1f41aba Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 17 Dec 2025 12:54:40 +0100 Subject: [PATCH 04/16] improve tests Signed-off-by: christian.lutnik --- .../flagd/FlagdProviderSyncResources.java | 2 +- .../flagd/FlagdProviderSyncResourcesCT.java | 177 ++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index fec52f8e6..ec4b8c9aa 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -36,7 +36,7 @@ public void setEnrichedContext(EvaluationContext context) { * @return true iff this was the first call to {@code initialize()} */ public synchronized boolean initialize() { - if (this.isInitialized) { + if (this.initialized || this.isShutDown) { return false; } this.isInitialized = true; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java new file mode 100644 index 000000000..c1cae0492 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java @@ -0,0 +1,177 @@ +package dev.openfeature.contrib.providers.flagd; + +import com.vmlens.api.AllInterleavings; +import com.vmlens.api.Runner; +import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +class FlagdProviderSyncResourcesCT { + private static final long MAX_TIME_TOLERANCE = 20; + + private FlagdProviderSyncResources flagdProviderSyncResources; + + @BeforeEach + void setUp() { + flagdProviderSyncResources = new FlagdProviderSyncResources(); + } + + @Timeout(2) + @Test + void waitForInitialization_failsWhenDeadlineElapses() { + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(2)); + } + + @Timeout(2) + @Test + void waitForInitialization_waitsApproxForDeadline() { + final AtomicLong start = new AtomicLong(); + final AtomicLong end = new AtomicLong(); + final long deadline = 45; + + start.set(System.currentTimeMillis()); + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); + end.set(System.currentTimeMillis()); + + final long elapsed = end.get() - start.get(); + // should wait at least for the deadline + Assertions.assertTrue(elapsed >= deadline); + // should not wait much longer than the deadline + Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE); + } + + @Timeout(2) + @Test + void interruptingWaitingThread_isIgnored() throws InterruptedException { + final AtomicBoolean isWaiting = new AtomicBoolean(); + final long deadline = 500; + Thread waitingThread = new Thread(() -> { + long start = System.currentTimeMillis(); + isWaiting.set(true); + Assertions.assertThrows( + GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); + + long end = System.currentTimeMillis(); + long duration = end - start; + // even though thread was interrupted, it still waited for the deadline + Assertions.assertTrue(duration >= deadline); + Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); + }); + waitingThread.start(); + + while (!isWaiting.get()) { + Thread.yield(); + } + + Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime + + for (int i = 0; i < 50; i++) { + waitingThread.interrupt(); + Thread.sleep(10); + } + + waitingThread.join(); + } + + @Timeout(5) + @Test + void callingInitialize_wakesUpWaitingThread() { + try (var interleavings = new AllInterleavings("calling initialize() wakes up waiting thread")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + flagdProviderSyncResources.waitForInitialization(10000); + endTime.set(System.currentTimeMillis()); + Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.initialize(); + } + ); + + Assertions.assertTrue(endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, () -> + "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms" + ); + } + } + } + + @Timeout(5) + @Test + void callingShutdown_wakesUpWaitingThreadWithException() { + try (var interleavings = new AllInterleavings("calling shutdown() wakes up waiting thread with exception")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + Assertions.assertThrows( + IllegalStateException.class, + () -> flagdProviderSyncResources.waitForInitialization(10000)); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.shutdown(); + } + ); + + Assertions.assertTrue(endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, () -> + "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms" + ); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializesWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.initialize() + ); + Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializeAndShutdownShutsDownWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.shutdown() + ); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + } + } + } + + @Timeout(2) + @Test + void waitForInitializationAfterCallingInitialize_returnsInstantly() { + flagdProviderSyncResources.initialize(); + long start = System.currentTimeMillis(); + flagdProviderSyncResources.waitForInitialization(10000); + long end = System.currentTimeMillis(); + // do not use MAX_TIME_TOLERANCE here, this should happen faster than that + Assertions.assertTrue(start + 1 >= end); + } +} From 009195b1927457849fa35d533b6925196109779f Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 10:00:21 +0100 Subject: [PATCH 05/16] migrate tests to vmlens, disable false positive test Signed-off-by: christian.lutnik --- providers/flagd/pom.xml | 4 +- .../resolver/process/model/FeatureFlag.java | 1 - ...roviderCT.java => FlagdProviderCTest.java} | 83 +++++++++--------- ...a => FlagdProviderSyncResourcesCTest.java} | 87 ++++++++----------- .../providers/flagd/FlagdProviderTest.java | 9 -- .../providers/flagd/FlagdTestUtils.java | 12 +-- 6 files changed, 87 insertions(+), 109 deletions(-) rename providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/{FlagdProviderCT.java => FlagdProviderCTest.java} (55%) rename providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/{FlagdProviderSyncResourcesCT.java => FlagdProviderSyncResourcesCTest.java} (72%) diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml index 6e7625814..dda9f9783 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -19,7 +19,7 @@ 1.79.0 3.25.6 - 1.2.22 + 1.2.24 flagd @@ -301,7 +301,7 @@ - **/*CT.java + **/*CTest.java true diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index e8aaedafe..b9b549f57 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import lombok.EqualsAndHashCode; import lombok.Getter; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCTest.java similarity index 55% rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCTest.java index e323f79e2..3b9536876 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCTest.java @@ -12,84 +12,85 @@ import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -class FlagdProviderCT { +class FlagdProviderCTest { private FlagdProvider provider; @BeforeEach void setup() throws Exception { - provider = FlagdTestUtils.createInProcessProvider( - Map.of( - "flag", - new FeatureFlag( - "ENABLED", - "a", - Map.of("a", "a", "b", "b", "c", "c"), - "{\n" - + " \"if\": [\n" - + " {\n" - + " \"ends_with\": [\n" - + " {\n" - + " \"var\": \"email\"\n" - + " },\n" - + " \"@ingen.com\"\n" - + " ]\n" - + " },\n" - + " \"b\",\n" - + " \"c\"\n" - + " ]\n" - + " }", - null - ) - ) - ); + provider = FlagdTestUtils.createInProcessProvider(Map.of( + "flag", + new FeatureFlag( + "ENABLED", + "a", + Map.of("a", "a", "b", "b", "c", "c"), + "{\n" + + " \"if\": [\n" + + " {\n" + + " \"ends_with\": [\n" + + " {\n" + + " \"var\": \"email\"\n" + + " },\n" + + " \"@openfeature.dev\"\n" + + " ]\n" + + " },\n" + + " \"b\",\n" + + " \"c\"\n" + + " ]\n" + + " }", + null))); provider.initialize(ImmutableContext.EMPTY); } @Test + @Disabled( + "There is a race condition in the JsonLogic library, but it does not affect us as we don't add operators after program startup") void concurrentFlagEvaluationsWork() { var invocationContext = ImmutableContext.EMPTY; try (var interleavings = new AllInterleavings("Concurrent Flag evaluations")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> assertEquals("c", - provider.getStringEvaluation("flag", "z", invocationContext).getValue()), - () -> assertEquals("c", - provider.getStringEvaluation("flag", "z", invocationContext).getValue()) - ); + () -> assertEquals( + "c", + provider.getStringEvaluation("flag", "z", invocationContext) + .getValue()), + () -> assertEquals( + "c", + provider.getStringEvaluation("flag", "z", invocationContext) + .getValue())); } } } @Test void flagEvaluationsWhileSettingContextWork() { - var invocationContext = ImmutableContext.EMPTY; - OpenFeatureAPI.getInstance().setProviderAndWait(provider); var client = OpenFeatureAPI.getInstance().getClient(); - var context = new ImmutableContext(Map.of("email", new Value("someone@ingen.com"))); + var invocationContext = ImmutableContext.EMPTY; + var clientContext = new ImmutableContext(Map.of("email", new Value("someone@openfeature.dev"))); try (var interleavings = new AllInterleavings("Concurrently setting client context and evaluating a Flag")) { while (interleavings.hasNext()) { Runner.runParallel( () -> assertTrue(List.of("b", "c") - .contains(provider.getStringEvaluation("flag", "z", invocationContext).getValue())), - () -> client.setEvaluationContext(context) - ); + .contains(provider.getStringEvaluation("flag", "z", invocationContext) + .getValue())), + () -> client.setEvaluationContext(clientContext)); } } } @Test void settingDifferentContextsWorks() { - OpenFeatureAPI.getInstance().setProviderAndWait(provider); var client = OpenFeatureAPI.getInstance().getClient(); - var clientContext = new ImmutableContext(Map.of("email", new Value("someone@ingen.com"))); + var invocationContext = ImmutableContext.EMPTY; + var clientContext = new ImmutableContext(Map.of("email", new Value("someone@openfeature.dev"))); var apiContext = new ImmutableContext(Map.of("email", new Value("someone.else@test.com"))); try (var interleavings = new AllInterleavings("Concurrently setting client and api context")) { @@ -98,8 +99,8 @@ void settingDifferentContextsWorks() { () -> client.setEvaluationContext(clientContext), () -> OpenFeatureAPI.getInstance().setEvaluationContext(apiContext), () -> assertTrue(List.of("b", "c") - .contains(provider.getStringEvaluation("flag", "z", ImmutableContext.EMPTY).getValue())) - ); + .contains(provider.getStringEvaluation("flag", "z", invocationContext) + .getValue()))); } } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java similarity index 72% rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index c1cae0492..f5f777334 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -3,16 +3,13 @@ import com.vmlens.api.AllInterleavings; import com.vmlens.api.Runner; import dev.openfeature.sdk.exceptions.GeneralError; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -class FlagdProviderSyncResourcesCT { +class FlagdProviderSyncResourcesCTest { private static final long MAX_TIME_TOLERANCE = 20; private FlagdProviderSyncResources flagdProviderSyncResources; @@ -49,34 +46,29 @@ void waitForInitialization_waitsApproxForDeadline() { @Timeout(2) @Test void interruptingWaitingThread_isIgnored() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final long deadline = 500; - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows( - GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); - - long end = System.currentTimeMillis(); - long duration = end - start; - // even though thread was interrupted, it still waited for the deadline - Assertions.assertTrue(duration >= deadline); - Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - for (int i = 0; i < 50; i++) { - waitingThread.interrupt(); - Thread.sleep(10); + try (var interleavings = new AllInterleavings("calling interrupt on a waiting thread is ignored")) { + final long deadline = 10; + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + var waitingThread = new Thread(() -> { + startTime.set(System.currentTimeMillis()); + Assertions.assertThrows( + GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); + endTime.set(System.currentTimeMillis()); + }); + waitingThread.start(); + + waitingThread.interrupt(); + + waitingThread.join(); + + long duration = endTime.get() - startTime.get(); + // even though thread was interrupted, it still waited for the deadline + Assertions.assertTrue(duration >= deadline); + Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); + } } - - waitingThread.join(); } @Timeout(5) @@ -95,13 +87,12 @@ void callingInitialize_wakesUpWaitingThread() { () -> { startTime.set(System.currentTimeMillis()); flagdProviderSyncResources.initialize(); - } - ); + }); - Assertions.assertTrue(endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, () -> - "Expected waiting thread to be released shortly after initialization, but waited for " - + (endTime.get() - startTime.get()) + "ms" - ); + Assertions.assertTrue( + endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, + () -> "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms"); } } } @@ -118,19 +109,19 @@ void callingShutdown_wakesUpWaitingThreadWithException() { Assertions.assertThrows( IllegalStateException.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + endTime.set(System.currentTimeMillis()); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); }, () -> { startTime.set(System.currentTimeMillis()); flagdProviderSyncResources.shutdown(); - } - ); + }); - Assertions.assertTrue(endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, () -> - "Expected waiting thread to be released shortly after initialization, but waited for " - + (endTime.get() - startTime.get()) + "ms" - ); + Assertions.assertTrue( + endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, + () -> "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms"); } } } @@ -141,9 +132,7 @@ void concurrentInitializesWork() { try (var interleavings = new AllInterleavings("concurrent initialize() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> flagdProviderSyncResources.initialize(), - () -> flagdProviderSyncResources.initialize() - ); + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.initialize()); Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); } } @@ -152,12 +141,10 @@ void concurrentInitializesWork() { @Timeout(5) @Test void concurrentInitializeAndShutdownShutsDownWork() { - try (var interleavings = new AllInterleavings("concurrent initialize() calls work")) { + try (var interleavings = new AllInterleavings("concurrent initialize() and shutdown() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> flagdProviderSyncResources.initialize(), - () -> flagdProviderSyncResources.shutdown() - ); + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown()); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); } 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 0fcc47a39..cf6cba722 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 @@ -20,12 +20,6 @@ import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelConnector; 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.contrib.providers.flagd.resolver.process.storage.StorageStateChange; -import dev.openfeature.contrib.providers.flagd.resolver.rpc.RpcResolver; -import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.Cache; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanRequest; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveFloatResponse; @@ -33,7 +27,6 @@ import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveObjectResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveStringResponse; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceBlockingStub; -import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceStub; import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.FlagEvaluationDetails; @@ -54,13 +47,11 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java index fdcb0adbc..3fcdf67c9 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java @@ -1,5 +1,7 @@ package dev.openfeature.contrib.providers.flagd; +import static org.mockito.Mockito.mock; + import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelConnector; import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver; @@ -13,12 +15,9 @@ import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; -import static org.mockito.Mockito.mock; - class FlagdTestUtils { // test helper // create provider with given grpc provider and state supplier @@ -31,7 +30,9 @@ static FlagdProvider createProvider(ChannelConnector connector, ServiceGrpc.Serv // create provider with given grpc provider, cache and state supplier static FlagdProvider createProvider( - ChannelConnector connector, Cache cache, ServiceGrpc.ServiceStub mockStub, + ChannelConnector connector, + Cache cache, + ServiceGrpc.ServiceStub mockStub, ServiceGrpc.ServiceBlockingStub mockBlockingStub) { final FlagdOptions flagdOptions = FlagdOptions.builder().build(); final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (connectionEvent) -> {}); @@ -64,8 +65,7 @@ static FlagdProvider createInProcessProvider(Map mockFlags) .build(); final FlagdProvider provider = new FlagdProvider(flagdOptions); final MockStorage mockStorage = new MockStorage( - mockFlags, - new LinkedBlockingQueue<>(Arrays.asList(new StorageStateChange(StorageState.OK)))); + mockFlags, new LinkedBlockingQueue<>(Arrays.asList(new StorageStateChange(StorageState.OK)))); try { final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); From d4f8d726c4e98d8f1ecb874454d989cf46b10c99 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 10:15:07 +0100 Subject: [PATCH 06/16] merge with main Signed-off-by: christian.lutnik --- .../FlagdProviderSyncResourcesCTest.java | 57 +++++++++++++++++++ .../providers/flagd/FlagdTestUtils.java | 12 ++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index f5f777334..8e0f1582c 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -2,6 +2,7 @@ import com.vmlens.api.AllInterleavings; import com.vmlens.api.Runner; +import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.GeneralError; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.Assertions; @@ -126,6 +127,36 @@ void callingShutdown_wakesUpWaitingThreadWithException() { } } + @Timeout(5) + @Test + void callingFatalError_wakesUpWaitingThreadWithException() { + try (var interleavings = new AllInterleavings( + "calling setFatal(true) wakes up waiting thread with exception")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + Assertions.assertThrows( + FatalError.class, + () -> flagdProviderSyncResources.waitForInitialization(10000)); + endTime.set(System.currentTimeMillis()); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.setFatal(true); + }); + + Assertions.assertTrue( + endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, + () -> "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms"); + } + } + } + @Timeout(5) @Test void concurrentInitializesWork() { @@ -151,6 +182,21 @@ void concurrentInitializeAndShutdownShutsDownWork() { } } + @Timeout(5) + @Test + void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown(), + () -> flagdProviderSyncResources.setFatal(true)); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + Assertions.assertTrue(flagdProviderSyncResources.isFatal()); + } + } + } + @Timeout(2) @Test void waitForInitializationAfterCallingInitialize_returnsInstantly() { @@ -161,4 +207,15 @@ void waitForInitializationAfterCallingInitialize_returnsInstantly() { // do not use MAX_TIME_TOLERANCE here, this should happen faster than that Assertions.assertTrue(start + 1 >= end); } + + @Timeout(2) + @Test + void waitForInitializationAfterCallingFatal_returnsInstantly() { + flagdProviderSyncResources.setFatal(true); + long start = System.currentTimeMillis(); + Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + long end = System.currentTimeMillis(); + // do not use MAX_TIME_TOLERANCE here, this should happen faster than that + Assertions.assertTrue(start + 1 >= end); + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java index 3fcdf67c9..fe8b6a5d8 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java @@ -11,10 +11,11 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; import dev.openfeature.contrib.providers.flagd.resolver.rpc.RpcResolver; import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.Cache; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.CacheType; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; import java.lang.reflect.Field; -import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; @@ -22,7 +23,7 @@ class FlagdTestUtils { // test helper // create provider with given grpc provider and state supplier static FlagdProvider createProvider(ChannelConnector connector, ServiceGrpc.ServiceBlockingStub mockBlockingStub) { - final Cache cache = new Cache("lru", 5); + final Cache cache = new Cache(CacheType.LRU.getValue(), 5); final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); return createProvider(connector, cache, mockStub, mockBlockingStub); @@ -35,7 +36,7 @@ static FlagdProvider createProvider( ServiceGrpc.ServiceStub mockStub, ServiceGrpc.ServiceBlockingStub mockBlockingStub) { final FlagdOptions flagdOptions = FlagdOptions.builder().build(); - final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (connectionEvent) -> {}); + final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (event, details, data) -> {}); try { Field resolver = RpcResolver.class.getDeclaredField("connector"); @@ -53,8 +54,7 @@ static FlagdProvider createProvider( } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } - final FlagdProvider provider = new FlagdProvider(grpcResolver, true); - return provider; + return new FlagdProvider(grpcResolver, true); } static FlagdProvider createInProcessProvider(Map mockFlags) { @@ -65,7 +65,7 @@ static FlagdProvider createInProcessProvider(Map mockFlags) .build(); final FlagdProvider provider = new FlagdProvider(flagdOptions); final MockStorage mockStorage = new MockStorage( - mockFlags, new LinkedBlockingQueue<>(Arrays.asList(new StorageStateChange(StorageState.OK)))); + mockFlags, new LinkedBlockingQueue<>(List.of(new StorageStateChange(StorageState.OK)))); try { final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); From ffc14b086ea21668ba0458a27e09f53162ef73b6 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 10:32:18 +0100 Subject: [PATCH 07/16] fix fatal abort on wait Signed-off-by: christian.lutnik --- .../FlagdProviderSyncResourcesCTest.java | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index 8e0f1582c..29292ef8c 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -24,6 +24,9 @@ void setUp() { @Test void waitForInitialization_failsWhenDeadlineElapses() { Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(2)); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); } @Timeout(2) @@ -42,6 +45,9 @@ void waitForInitialization_waitsApproxForDeadline() { Assertions.assertTrue(elapsed >= deadline); // should not wait much longer than the deadline Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); } @Timeout(2) @@ -68,6 +74,9 @@ void interruptingWaitingThread_isIgnored() throws InterruptedException { // even though thread was interrupted, it still waited for the deadline Assertions.assertTrue(duration >= deadline); Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); } } } @@ -84,6 +93,8 @@ void callingInitialize_wakesUpWaitingThread() { flagdProviderSyncResources.waitForInitialization(10000); endTime.set(System.currentTimeMillis()); Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); }, () -> { startTime.set(System.currentTimeMillis()); @@ -112,6 +123,7 @@ void callingShutdown_wakesUpWaitingThreadWithException() { () -> flagdProviderSyncResources.waitForInitialization(10000)); endTime.set(System.currentTimeMillis()); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); }, () -> { @@ -142,7 +154,8 @@ void callingFatalError_wakesUpWaitingThreadWithException() { () -> flagdProviderSyncResources.waitForInitialization(10000)); endTime.set(System.currentTimeMillis()); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); - Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + Assertions.assertTrue(flagdProviderSyncResources.isFatal()); }, () -> { startTime.set(System.currentTimeMillis()); @@ -197,6 +210,21 @@ void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { } } + @Timeout(5) + @Test + void concurrentInitializeAndSetFatalShutsDownWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() and fatal() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.setFatal(true)); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + Assertions.assertTrue(flagdProviderSyncResources.isFatal()); + } + } + } + @Timeout(2) @Test void waitForInitializationAfterCallingInitialize_returnsInstantly() { @@ -208,6 +236,17 @@ void waitForInitializationAfterCallingInitialize_returnsInstantly() { Assertions.assertTrue(start + 1 >= end); } + @Timeout(2) + @Test + void waitForInitializationAfterCallingShutdown_returnsInstantly() { + flagdProviderSyncResources.shutdown(); + long start = System.currentTimeMillis(); + flagdProviderSyncResources.waitForInitialization(10000); + long end = System.currentTimeMillis(); + // do not use MAX_TIME_TOLERANCE here, this should happen faster than that + Assertions.assertTrue(start + 1 >= end); + } + @Timeout(2) @Test void waitForInitializationAfterCallingFatal_returnsInstantly() { From bc6c09897743cad3e3aafc09e9a874a7400fd8e4 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 10:40:27 +0100 Subject: [PATCH 08/16] fix tests Signed-off-by: christian.lutnik --- .../providers/flagd/FlagdProviderSyncResourcesCTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index 29292ef8c..d3ba6462b 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -241,10 +241,10 @@ void waitForInitializationAfterCallingInitialize_returnsInstantly() { void waitForInitializationAfterCallingShutdown_returnsInstantly() { flagdProviderSyncResources.shutdown(); long start = System.currentTimeMillis(); - flagdProviderSyncResources.waitForInitialization(10000); + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 1 >= end); + Assertions.assertTrue(start + 3 >= end); } @Timeout(2) @@ -255,6 +255,6 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 1 >= end); + Assertions.assertTrue(start + 3 >= end); } } From 02fb44df0fef26084e4bb4f8c5a9ed044e677087 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 10:50:29 +0100 Subject: [PATCH 09/16] get data Signed-off-by: christian.lutnik --- .../providers/flagd/FlagdProviderSyncResourcesCTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index d3ba6462b..0947318b6 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -198,7 +198,8 @@ void concurrentInitializeAndShutdownShutsDownWork() { @Timeout(5) @Test void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { - try (var interleavings = new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { + try (var interleavings = new AllInterleavings( + "concurrent initialize() and shutdown() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown(), @@ -255,6 +256,7 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); // do not use MAX_TIME_TOLERANCE here, this should happen faster than that + Assertions.assertEquals(start, end, "started at " + start + " ended at " + end); Assertions.assertTrue(start + 3 >= end); } } From 6a464caf63f928098a5a14d41b810f64caf5efaf Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 11:16:57 +0100 Subject: [PATCH 10/16] get data Signed-off-by: christian.lutnik --- .../providers/flagd/FlagdProviderSyncResourcesCTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index 0947318b6..b975471c3 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -44,7 +44,7 @@ void waitForInitialization_waitsApproxForDeadline() { // should wait at least for the deadline Assertions.assertTrue(elapsed >= deadline); // should not wait much longer than the deadline - Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE); + Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE, "elapsed time: " + elapsed +" deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isFatal()); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); @@ -245,7 +245,7 @@ void waitForInitializationAfterCallingShutdown_returnsInstantly() { Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 3 >= end); + Assertions.assertTrue(start + 5 >= end); } @Timeout(2) @@ -256,7 +256,6 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertEquals(start, end, "started at " + start + " ended at " + end); - Assertions.assertTrue(start + 3 >= end); + Assertions.assertTrue(start + 5 >= end); } } From fd896c299658d2a3e027320b95fb46619eeae136 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 11:17:35 +0100 Subject: [PATCH 11/16] format Signed-off-by: christian.lutnik --- .../FlagdProviderSyncResourcesCTest.java | 21 ++++++++++--------- .../providers/flagd/FlagdTestUtils.java | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index b975471c3..41940b322 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -44,7 +44,9 @@ void waitForInitialization_waitsApproxForDeadline() { // should wait at least for the deadline Assertions.assertTrue(elapsed >= deadline); // should not wait much longer than the deadline - Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE, "elapsed time: " + elapsed +" deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); + Assertions.assertTrue( + elapsed < deadline + MAX_TIME_TOLERANCE, + "elapsed time: " + elapsed + " deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isFatal()); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); @@ -142,16 +144,15 @@ void callingShutdown_wakesUpWaitingThreadWithException() { @Timeout(5) @Test void callingFatalError_wakesUpWaitingThreadWithException() { - try (var interleavings = new AllInterleavings( - "calling setFatal(true) wakes up waiting thread with exception")) { + try (var interleavings = + new AllInterleavings("calling setFatal(true) wakes up waiting thread with exception")) { while (interleavings.hasNext()) { final var startTime = new AtomicLong(); final var endTime = new AtomicLong(); Runner.runParallel( () -> { Assertions.assertThrows( - FatalError.class, - () -> flagdProviderSyncResources.waitForInitialization(10000)); + FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); endTime.set(System.currentTimeMillis()); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); @@ -198,11 +199,12 @@ void concurrentInitializeAndShutdownShutsDownWork() { @Timeout(5) @Test void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { - try (var interleavings = new AllInterleavings( - "concurrent initialize() and shutdown() and fatal() calls work")) { + try (var interleavings = + new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown(), + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.shutdown(), () -> flagdProviderSyncResources.setFatal(true)); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); @@ -217,8 +219,7 @@ void concurrentInitializeAndSetFatalShutsDownWork() { try (var interleavings = new AllInterleavings("concurrent initialize() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> flagdProviderSyncResources.initialize(), - () -> flagdProviderSyncResources.setFatal(true)); + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.setFatal(true)); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); Assertions.assertTrue(flagdProviderSyncResources.isFatal()); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java index fe8b6a5d8..752a29222 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java @@ -64,8 +64,8 @@ static FlagdProvider createInProcessProvider(Map mockFlags) .deadline(1000) .build(); final FlagdProvider provider = new FlagdProvider(flagdOptions); - final MockStorage mockStorage = new MockStorage( - mockFlags, new LinkedBlockingQueue<>(List.of(new StorageStateChange(StorageState.OK)))); + final MockStorage mockStorage = + new MockStorage(mockFlags, new LinkedBlockingQueue<>(List.of(new StorageStateChange(StorageState.OK)))); try { final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); From f07158b0bd7d41331ee68194ae9a1c54cb86bead Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 11:27:16 +0100 Subject: [PATCH 12/16] increase tolerance Signed-off-by: christian.lutnik --- .../providers/flagd/FlagdProviderSyncResourcesCTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index 41940b322..3a795923e 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -46,7 +46,7 @@ void waitForInitialization_waitsApproxForDeadline() { // should not wait much longer than the deadline Assertions.assertTrue( elapsed < deadline + MAX_TIME_TOLERANCE, - "elapsed time: " + elapsed + " deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); + ()->"elapsed time: " + elapsed + " deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isFatal()); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); @@ -246,7 +246,7 @@ void waitForInitializationAfterCallingShutdown_returnsInstantly() { Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 5 >= end); + Assertions.assertTrue(start + 10 >= end); } @Timeout(2) @@ -257,6 +257,6 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 5 >= end); + Assertions.assertTrue(start + 10 >= end); } } From c9418a5e3b1e141dd21d545dfc8fcad89f0a4691 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 11:36:12 +0100 Subject: [PATCH 13/16] increase tolerance Signed-off-by: christian.lutnik --- .../providers/flagd/FlagdProviderSyncResourcesCTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index 3a795923e..cff77526d 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -256,7 +256,7 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { long start = System.currentTimeMillis(); Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); - // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 10 >= end); + // for some reason, throwing the exception takes very long + Assertions.assertTrue(start + MAX_TIME_TOLERANCE >= end); } } From 9073aa5c07a0be228692529eaecb60f576d2b97e Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 11:47:45 +0100 Subject: [PATCH 14/16] add tests Signed-off-by: christian.lutnik --- .../flagd/FlagdProviderSyncResources.java | 2 +- .../FlagdProviderSyncResourcesCTest.java | 38 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index ec4b8c9aa..47f3b0c9e 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -36,7 +36,7 @@ public void setEnrichedContext(EvaluationContext context) { * @return true iff this was the first call to {@code initialize()} */ public synchronized boolean initialize() { - if (this.initialized || this.isShutDown) { + if (this.isInitialized || this.isShutDown || this.isFatal) { return false; } this.isInitialized = true; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index cff77526d..fc8edb632 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -4,7 +4,9 @@ import com.vmlens.api.Runner; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.GeneralError; + import java.util.concurrent.atomic.AtomicLong; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,7 +48,7 @@ void waitForInitialization_waitsApproxForDeadline() { // should not wait much longer than the deadline Assertions.assertTrue( elapsed < deadline + MAX_TIME_TOLERANCE, - ()->"elapsed time: " + elapsed + " deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); + () -> "elapsed time: " + elapsed + " deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isFatal()); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); @@ -121,7 +123,7 @@ void callingShutdown_wakesUpWaitingThreadWithException() { Runner.runParallel( () -> { Assertions.assertThrows( - IllegalStateException.class, + GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); endTime.set(System.currentTimeMillis()); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); @@ -145,7 +147,7 @@ void callingShutdown_wakesUpWaitingThreadWithException() { @Test void callingFatalError_wakesUpWaitingThreadWithException() { try (var interleavings = - new AllInterleavings("calling setFatal(true) wakes up waiting thread with exception")) { + new AllInterleavings("calling setFatal(true) wakes up waiting thread with exception")) { while (interleavings.hasNext()) { final var startTime = new AtomicLong(); final var endTime = new AtomicLong(); @@ -200,7 +202,7 @@ void concurrentInitializeAndShutdownShutsDownWork() { @Test void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { try (var interleavings = - new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { + new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), @@ -220,7 +222,6 @@ void concurrentInitializeAndSetFatalShutsDownWork() { while (interleavings.hasNext()) { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.setFatal(true)); - Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); Assertions.assertTrue(flagdProviderSyncResources.isFatal()); } @@ -259,4 +260,31 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { // for some reason, throwing the exception takes very long Assertions.assertTrue(start + MAX_TIME_TOLERANCE >= end); } + + @Timeout(2) + @Test + void initializeAfterFatalReturnsFalse() { + flagdProviderSyncResources.setFatal(true); + Assertions.assertFalse(flagdProviderSyncResources.initialize()); + } + + @Timeout(2) + @Test + void initializeAfterShutdownReturnsFalse() { + flagdProviderSyncResources.shutdown(); + Assertions.assertFalse(flagdProviderSyncResources.initialize()); + } + + @Timeout(2) + @Test + void initializeAfterInitializeReturnsFalse() { + flagdProviderSyncResources.initialize(); + Assertions.assertFalse(flagdProviderSyncResources.initialize()); + } + + @Timeout(2) + @Test + void firstInitializeReturnsTrue() { + Assertions.assertTrue(flagdProviderSyncResources.initialize()); + } } From 776e653eb23a5f14333c9f7458f8b4d831f48633 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 21 Jan 2026 11:54:09 +0100 Subject: [PATCH 15/16] format Signed-off-by: christian.lutnik --- .../providers/flagd/FlagdProviderSyncResourcesCTest.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index fc8edb632..b59c1901a 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -4,9 +4,7 @@ import com.vmlens.api.Runner; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.GeneralError; - import java.util.concurrent.atomic.AtomicLong; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -123,8 +121,7 @@ void callingShutdown_wakesUpWaitingThreadWithException() { Runner.runParallel( () -> { Assertions.assertThrows( - GeneralError.class, - () -> flagdProviderSyncResources.waitForInitialization(10000)); + GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); endTime.set(System.currentTimeMillis()); Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertFalse(flagdProviderSyncResources.isFatal()); @@ -147,7 +144,7 @@ void callingShutdown_wakesUpWaitingThreadWithException() { @Test void callingFatalError_wakesUpWaitingThreadWithException() { try (var interleavings = - new AllInterleavings("calling setFatal(true) wakes up waiting thread with exception")) { + new AllInterleavings("calling setFatal(true) wakes up waiting thread with exception")) { while (interleavings.hasNext()) { final var startTime = new AtomicLong(); final var endTime = new AtomicLong(); @@ -202,7 +199,7 @@ void concurrentInitializeAndShutdownShutsDownWork() { @Test void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { try (var interleavings = - new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { + new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), From 2511ea132e8d93e756b37cd13db854153204402b Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 18 Feb 2026 17:10:06 -0500 Subject: [PATCH 16/16] fixup: from rebase Signed-off-by: Todd Baert --- .../flagd/FlagdProviderSyncResources.java | 2 + .../FlagdProviderSyncResourcesCTest.java | 94 +++++++ .../flagd/FlagdProviderSyncResourcesTest.java | 248 ------------------ 3 files changed, 96 insertions(+), 248 deletions(-) delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index 47f3b0c9e..976870a97 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -18,7 +18,9 @@ class FlagdProviderSyncResources { @Setter private volatile ProviderEvent previousEvent; + @Setter private volatile boolean isFatal; + private volatile ProviderEventDetails fatalProviderEventDetails; private volatile EvaluationContext enrichedContext = new ImmutableContext(); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index b59c1901a..f8c4d0977 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -2,9 +2,13 @@ import com.vmlens.api.AllInterleavings; import com.vmlens.api.Runner; +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.ProviderEventDetails; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -284,4 +288,94 @@ void initializeAfterInitializeReturnsFalse() { void firstInitializeReturnsTrue() { Assertions.assertTrue(flagdProviderSyncResources.initialize()); } + + @Timeout(2) + @Test + void fatalHasPrecedenceOverInitAndShutdown() { + flagdProviderSyncResources.fatalError(null); + flagdProviderSyncResources.initialize(); + flagdProviderSyncResources.shutdown(); + + Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + } + + @Timeout(2) + @Test + void fatalAbortsInit() throws InterruptedException { + final AtomicBoolean isWaiting = new AtomicBoolean(); + final AtomicLong waitTime = new AtomicLong(Long.MAX_VALUE); + final AtomicReference fatalException = new AtomicReference<>(); + + Thread waitingThread = new Thread(() -> { + long start = System.currentTimeMillis(); + isWaiting.set(true); + try { + flagdProviderSyncResources.waitForInitialization(10000); + } catch (Exception e) { + fatalException.set(e); + } + long end = System.currentTimeMillis(); + long duration = end - start; + waitTime.set(duration); + }); + waitingThread.start(); + + while (!isWaiting.get()) { + Thread.yield(); + } + + Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime + + var fatalEvent = ProviderEventDetails.builder() + .errorCode(ErrorCode.PROVIDER_FATAL) + .message("Some message") + .build(); + flagdProviderSyncResources.fatalError(fatalEvent); + + waitingThread.join(); + + var wait = MAX_TIME_TOLERANCE * 3; + + Assertions.assertTrue( + waitTime.get() < wait, + () -> "Wakeup should be almost instant, but took " + waitTime.get() + + " ms, which is more than the max of" + + wait + " ms"); + Assertions.assertNotNull(fatalException.get()); + Assertions.assertInstanceOf(FatalError.class, fatalException.get()); + Assertions.assertEquals( + "Initialization failed due to a fatal error: " + fatalEvent.getMessage(), + fatalException.get().getMessage()); + } + + @Timeout(2) + @Test + void callingShutdownWithPreviousNonFatal_wakesUpWaitingThread_WithGeneralException() throws InterruptedException { + final AtomicBoolean isWaiting = new AtomicBoolean(); + final AtomicBoolean successfulTest = new AtomicBoolean(); + + Thread waitingThread = new Thread(() -> { + long start = System.currentTimeMillis(); + isWaiting.set(true); + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + + long end = System.currentTimeMillis(); + long duration = end - start; + var wait = MAX_TIME_TOLERANCE * 3; + successfulTest.set(duration < wait); + }); + waitingThread.start(); + + while (!isWaiting.get()) { + Thread.yield(); + } + + Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime + + flagdProviderSyncResources.shutdown(); + + waitingThread.join(); + + Assertions.assertTrue(successfulTest.get()); + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java deleted file mode 100644 index dd5dbe73a..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package dev.openfeature.contrib.providers.flagd; - -import dev.openfeature.sdk.ErrorCode; -import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.exceptions.FatalError; -import dev.openfeature.sdk.exceptions.GeneralError; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -class FlagdProviderSyncResourcesTest { - private static final long MAX_TIME_TOLERANCE = 20; - - private FlagdProviderSyncResources flagdProviderSyncResources; - - @BeforeEach - void setUp() { - flagdProviderSyncResources = new FlagdProviderSyncResources(); - } - - @Timeout(2) - @Test - void waitForInitialization_failsWhenDeadlineElapses() { - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(2)); - } - - @Timeout(2) - @Test - void waitForInitialization_waitsApproxForDeadline() { - final AtomicLong start = new AtomicLong(); - final AtomicLong end = new AtomicLong(); - final long deadline = 45; - - start.set(System.currentTimeMillis()); - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); - end.set(System.currentTimeMillis()); - - final long elapsed = end.get() - start.get(); - // should wait at least for the deadline - Assertions.assertTrue(elapsed >= deadline); - // should not wait much longer than the deadline - Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE); - } - - @Timeout(2) - @Test - void interruptingWaitingThread_isIgnored() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final long deadline = 500; - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows( - GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); - - long end = System.currentTimeMillis(); - long duration = end - start; - // even though thread was interrupted, it still waited for the deadline - Assertions.assertTrue(duration >= deadline); - Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - for (int i = 0; i < 50; i++) { - waitingThread.interrupt(); - Thread.sleep(10); - } - - waitingThread.join(); - } - - @Timeout(2) - @Test - void callingInitialize_wakesUpWaitingThread() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicLong waitTime = new AtomicLong(Long.MAX_VALUE); - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - flagdProviderSyncResources.waitForInitialization(10000); - long end = System.currentTimeMillis(); - long duration = end - start; - waitTime.set(duration); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - flagdProviderSyncResources.initialize(); - - waitingThread.join(); - - var wait = MAX_TIME_TOLERANCE * 3; - - Assertions.assertTrue( - waitTime.get() < wait, - () -> "Wakeup should be almost instant, but took " + waitTime.get() - + " ms, which is more than the max of" - + wait + " ms"); - } - - @Timeout(2) - @Test - void callingShutdownWithPreviousNonFatal_wakesUpWaitingThread_WithGeneralException() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicBoolean successfulTest = new AtomicBoolean(); - - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); - - long end = System.currentTimeMillis(); - long duration = end - start; - var wait = MAX_TIME_TOLERANCE * 3; - successfulTest.set(duration < wait); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - flagdProviderSyncResources.shutdown(); - - waitingThread.join(); - - Assertions.assertTrue(successfulTest.get()); - } - - @Timeout(2) - @Test - void callingShutdownWithPreviousFatal_wakesUpWaitingThread_WithFatalException() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicBoolean successfulTest = new AtomicBoolean(); - flagdProviderSyncResources.fatalError(null); - - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); - - long end = System.currentTimeMillis(); - long duration = end - start; - var wait = MAX_TIME_TOLERANCE * 3; - successfulTest.set(duration < wait); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - flagdProviderSyncResources.shutdown(); - - waitingThread.join(); - - Assertions.assertTrue(successfulTest.get()); - } - - @Timeout(2) - @Test - void waitForInitializationAfterCallingInitialize_returnsInstantly() { - flagdProviderSyncResources.initialize(); - long start = System.currentTimeMillis(); - flagdProviderSyncResources.waitForInitialization(10000); - long end = System.currentTimeMillis(); - // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 1 >= end); - } - - @Timeout(2) - @Test - void fatalHasPrecedenceOverInitAndShutdown() { - flagdProviderSyncResources.fatalError(null); - flagdProviderSyncResources.initialize(); - flagdProviderSyncResources.shutdown(); - - Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); - } - - @Timeout(2) - @Test - void fatalAbortsInit() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicLong waitTime = new AtomicLong(Long.MAX_VALUE); - final AtomicReference fatalException = new AtomicReference<>(); - - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - try { - flagdProviderSyncResources.waitForInitialization(10000); - } catch (Exception e) { - fatalException.set(e); - } - long end = System.currentTimeMillis(); - long duration = end - start; - waitTime.set(duration); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - var fatalEvent = ProviderEventDetails.builder() - .errorCode(ErrorCode.PROVIDER_FATAL) - .message("Some message") - .build(); - flagdProviderSyncResources.fatalError(fatalEvent); - - waitingThread.join(); - - var wait = MAX_TIME_TOLERANCE * 3; - - Assertions.assertTrue( - waitTime.get() < wait, - () -> "Wakeup should be almost instant, but took " + waitTime.get() - + " ms, which is more than the max of" - + wait + " ms"); - Assertions.assertNotNull(fatalException.get()); - Assertions.assertInstanceOf(FatalError.class, fatalException.get()); - Assertions.assertEquals( - "Initialization failed due to a fatal error: " + fatalEvent.getMessage(), - fatalException.get().getMessage()); - } -}