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());
+ }
+}