diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml index c26ff014b..1ae99ab3f 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -19,6 +19,7 @@ 1.77.0 3.25.6 + 1.2.22 flagd @@ -175,6 +176,13 @@ 2.0.17 test + + + com.vmlens + api + ${com.vmlens.version} + test + @@ -288,6 +296,25 @@ dev.openfeature.contrib.- + + com.vmlens + vmlens-maven-plugin + ${com.vmlens.version} + + + test + + test + + + + **/*CT.java + + true + + + + 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 e54c938cf..86ae34604 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 @@ -166,7 +166,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(); } @@ -175,7 +175,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(); } @@ -210,7 +210,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(); } @@ -235,11 +235,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()) { @@ -260,7 +260,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 new file mode 100644 index 000000000..e323f79e2 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java @@ -0,0 +1,106 @@ +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 java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FlagdProviderCT { + 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.initialize(ImmutableContext.EMPTY); + } + + @Test + 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()) + ); + } + } + } + + @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("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())) + ); + } + } + } +} 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 115887002..4c39924e4 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 @@ -152,7 +152,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()); @@ -230,7 +230,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()); @@ -294,7 +294,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); @@ -376,7 +376,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); @@ -415,7 +415,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); @@ -439,7 +439,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()); @@ -498,7 +498,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 @@ -676,66 +676,4 @@ void updatesSyncMetadataWithCallback() throws Exception { assertEquals(val, contextFromHook.get().getValue(key).asString()); } } - - // 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, (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; - } - - // 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()); + } +}