Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions providers/flagd/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<io.grpc.version>1.77.0</io.grpc.version>
<!-- caution - updating this will break compatibility with older protobuf-java versions -->
<protobuf-java.min.version>3.25.6</protobuf-java.min.version>
<com.vmlens.version>1.2.22</com.vmlens.version>
</properties>

<name>flagd</name>
Expand Down Expand Up @@ -175,6 +176,13 @@
<version>2.0.17</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.vmlens</groupId>
<artifactId>api</artifactId>
<version>${com.vmlens.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
Expand Down Expand Up @@ -288,6 +296,25 @@
<onlyAnalyze>dev.openfeature.contrib.-</onlyAnalyze>
</configuration>
</plugin>
<plugin>
<groupId>com.vmlens</groupId>
<artifactId>vmlens-maven-plugin</artifactId>
<version>${com.vmlens.version}</version>
<executions>
<execution>
<id>test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*CT.java</include>
</includes>
<failIfNoTests>true</failIfNoTests>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationC
return ProviderEvaluation.<T>builder()
.errorMessage("flag: " + key + " not found")
.errorCode(ErrorCode.FLAG_NOT_FOUND)
.flagMetadata(getFlagMetadata(storageQueryResult))
.flagMetadata(getFlagMetadata(storageQueryResult, scope))
.build();
}

Expand All @@ -175,7 +175,7 @@ private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationC
return ProviderEvaluation.<T>builder()
.errorMessage("flag: " + key + " is disabled")
.errorCode(ErrorCode.FLAG_NOT_FOUND)
.flagMetadata(getFlagMetadata(storageQueryResult))
.flagMetadata(getFlagMetadata(storageQueryResult, scope))
.build();
}

Expand Down Expand Up @@ -210,7 +210,7 @@ private <T> ProviderEvaluation<T> resolve(Class<T> 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();
}

Expand All @@ -235,11 +235,11 @@ private <T> ProviderEvaluation<T> resolve(Class<T> 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<String, Object> entry :
storageQueryResult.getFlagSetMetadata().entrySet()) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -51,7 +52,7 @@ public FeatureFlag(String state, String defaultVariant, Map<String, Object> vari
this.defaultVariant = defaultVariant;
this.variants = variants;
this.targeting = targeting;
this.metadata = new HashMap<>();
this.metadata = Collections.emptyMap();
}

/** Get targeting rule of the flag. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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("[email protected]")));

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("[email protected]")));
var apiContext = new ImmutableContext(Map.of("email", new Value("[email protected]")));

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()))
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
assertTrue(booleanDetails.getValue());
Expand Down Expand Up @@ -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<Boolean> booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
assertEquals(false, booleanDetails.getValue());
Expand Down Expand Up @@ -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<Boolean> booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<Boolean> booleanDetails =
api.getClient().getBooleanDetails(FLAG_KEY, false, new MutableContext());
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String, FeatureFlag>(),
new LinkedBlockingQueue<StorageStateChange>(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;
}
}
Loading
Loading