diff --git a/providers/flagd/README.md b/providers/flagd/README.md
index 5144a4749..4c6786efb 100644
--- a/providers/flagd/README.md
+++ b/providers/flagd/README.md
@@ -47,6 +47,13 @@ FlagdProvider flagdProvider = new FlagdProvider(
In the above example, in-process handlers attempt to connect to a sync service on address `localhost:8013` to obtain [flag definitions](https://github.com/open-feature/schemas/blob/main/json/flags.json).
+#### Sync-metadata
+
+To support the injection of contextual data configured in flagd for in-process evaluation, the provider exposes a `getSyncMetadata` accessor which provides the most recent value returned by the [GetMetadata RPC](https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata).
+The value is updated with every (re)connection to the sync implementation.
+This can be used to enrich evaluations with such data.
+If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map.
+
#### Offline mode
In-process resolvers can also work in an offline mode.
diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml
index 62fae18a2..dd3648239 100644
--- a/providers/flagd/pom.xml
+++ b/providers/flagd/pom.xml
@@ -19,7 +19,7 @@
flagd
- FlagD provider for Java
+ flagd provider for Java
https://openfeature.dev
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
index a396d00f8..15bfb5cbb 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
@@ -1,8 +1,10 @@
package dev.openfeature.contrib.providers.flagd;
-import java.util.List;
+import java.util.Collections;
+import java.util.Map;
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver;
@@ -20,10 +22,11 @@
@Slf4j
@SuppressWarnings({ "PMD.TooManyStaticImports", "checkstyle:NoFinalizer" })
public class FlagdProvider extends EventProvider {
- private static final String FLAGD_PROVIDER = "flagD Provider";
+ private static final String FLAGD_PROVIDER = "flagd";
private final Resolver flagResolver;
private volatile boolean initialized = false;
private volatile boolean connected = false;
+ private volatile Map syncMetadata = Collections.emptyMap();
private EvaluationContext evaluationContext;
@@ -47,13 +50,13 @@ public FlagdProvider(final FlagdOptions options) {
switch (options.getResolverType().asString()) {
case Config.RESOLVER_IN_PROCESS:
this.flagResolver = new InProcessResolver(options, this::isConnected,
- this::onResolverConnectionChanged);
+ this::onConnectionEvent);
break;
case Config.RESOLVER_RPC:
this.flagResolver = new GrpcResolver(options,
new Cache(options.getCacheType(), options.getMaxCacheSize()),
this::isConnected,
- this::onResolverConnectionChanged);
+ this::onConnectionEvent);
break;
default:
throw new IllegalStateException(
@@ -117,6 +120,19 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa
return this.flagResolver.objectEvaluation(key, defaultValue, mergeContext(ctx));
}
+ /**
+ * An unmodifiable view of an object map representing the latest result of the
+ * SyncMetadata.
+ * Set on initial connection and updated with every reconnection.
+ * see:
+ * https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata
+ *
+ * @return Object map representing sync metadata
+ */
+ protected Map getSyncMetadata() {
+ return Collections.unmodifiableMap(syncMetadata);
+ }
+
private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) {
if (this.evaluationContext != null) {
return evaluationContext.merge(clientCallCtx);
@@ -129,15 +145,16 @@ private boolean isConnected() {
return this.connected;
}
- private void onResolverConnectionChanged(boolean newConnectedState, List changedFlagKeys) {
+ private void onConnectionEvent(ConnectionEvent connectionEvent) {
boolean previous = connected;
- boolean current = newConnectedState;
- this.connected = newConnectedState;
+ boolean current = connected = connectionEvent.isConnected();
+ syncMetadata = connectionEvent.getSyncMetadata();
// configuration changed
if (initialized && previous && current) {
log.debug("Configuration changed");
- ProviderEventDetails details = ProviderEventDetails.builder().flagsChanged(changedFlagKeys)
+ ProviderEventDetails details = ProviderEventDetails.builder()
+ .flagsChanged(connectionEvent.getFlagsChanged())
.message("configuration changed").build();
this.emitProviderConfigurationChanged(details);
return;
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java
index 8976a6789..45d82b66b 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java
@@ -5,7 +5,7 @@
import dev.openfeature.sdk.Value;
/**
- * A generic flag resolving contract for flagd.
+ * Abstraction that resolves flag values in from some source.
*/
public interface Resolver {
void init() throws Exception;
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java
new file mode 100644
index 000000000..cc8a7a0b0
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java
@@ -0,0 +1,68 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Event payload for a
+ * {@link dev.openfeature.contrib.providers.flagd.resolver.Resolver} connection
+ * state change event.
+ */
+@AllArgsConstructor
+public class ConnectionEvent {
+ @Getter
+ private final boolean connected;
+ private final List flagsChanged;
+ private final Map syncMetadata;
+
+ /**
+ * Construct a new ConnectionEvent.
+ *
+ * @param connected status of the connection
+ */
+ public ConnectionEvent(boolean connected) {
+ this(connected, Collections.emptyList(), Collections.emptyMap());
+ }
+
+ /**
+ * Construct a new ConnectionEvent.
+ *
+ * @param connected status of the connection
+ * @param flagsChanged list of flags changed
+ */
+ public ConnectionEvent(boolean connected, List flagsChanged) {
+ this(connected, flagsChanged, Collections.emptyMap());
+ }
+
+ /**
+ * Construct a new ConnectionEvent.
+ *
+ * @param connected status of the connection
+ * @param syncMetadata sync.getMetadata
+ */
+ public ConnectionEvent(boolean connected, Map syncMetadata) {
+ this(connected, Collections.emptyList(), syncMetadata);
+ }
+
+ /**
+ * Get changed flags.
+ *
+ * @return an unmodifiable view of the changed flags
+ */
+ public List getFlagsChanged() {
+ return Collections.unmodifiableList(flagsChanged);
+ }
+
+ /**
+ * Get changed sync metadata.
+ *
+ * @return an unmodifiable view of the sync metadata
+ */
+ public Map getSyncMetadata() {
+ return Collections.unmodifiableMap(syncMetadata);
+ }
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java
new file mode 100644
index 000000000..939b614d3
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java
@@ -0,0 +1,184 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Message;
+import com.google.protobuf.NullValue;
+import com.google.protobuf.Struct;
+
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.MutableStructure;
+import dev.openfeature.sdk.Structure;
+import dev.openfeature.sdk.Value;
+
+/**
+ * gRPC type conversion utils.
+ */
+public class Convert {
+ /**
+ * Recursively convert protobuf structure to openfeature value.
+ */
+ public static Value convertObjectResponse(Struct protobuf) {
+ return convertProtobufMap(protobuf.getFieldsMap());
+ }
+
+ /**
+ * Recursively convert the Evaluation context to a protobuf structure.
+ */
+ public static Struct convertContext(EvaluationContext ctx) {
+ Map ctxMap = ctx.asMap();
+ // asMap() does not provide explicitly set targeting key (ex:- new
+ // ImmutableContext("TargetingKey") ).
+ // Hence, we add this explicitly here for targeting rule processing.
+ ctxMap.put("targetingKey", new Value(ctx.getTargetingKey()));
+
+ return convertMap(ctxMap).getStructValue();
+ }
+
+ /**
+ * Convert any openfeature value to a protobuf value.
+ */
+ public static com.google.protobuf.Value convertAny(Value value) {
+ if (value.isList()) {
+ return convertList(value.asList());
+ } else if (value.isStructure()) {
+ return convertMap(value.asStructure().asMap());
+ } else {
+ return convertPrimitive(value);
+ }
+ }
+
+ /**
+ * Convert any protobuf value to {@link Value}.
+ */
+ public static Value convertAny(com.google.protobuf.Value protobuf) {
+ if (protobuf.hasListValue()) {
+ return convertList(protobuf.getListValue());
+ } else if (protobuf.hasStructValue()) {
+ return convertProtobufMap(protobuf.getStructValue().getFieldsMap());
+ } else {
+ return convertPrimitive(protobuf);
+ }
+ }
+
+ /**
+ * Convert OpenFeature map to protobuf {@link com.google.protobuf.Value}.
+ */
+ public static com.google.protobuf.Value convertMap(Map map) {
+ Map values = new HashMap<>();
+
+ map.keySet().forEach((String key) -> {
+ Value value = map.get(key);
+ values.put(key, convertAny(value));
+ });
+ Struct struct = Struct.newBuilder()
+ .putAllFields(values).build();
+ return com.google.protobuf.Value.newBuilder().setStructValue(struct).build();
+ }
+
+ /**
+ * Convert protobuf map with {@link com.google.protobuf.Value} to OpenFeature
+ * map.
+ */
+ public static Value convertProtobufMap(Map map) {
+ return new Value(convertProtobufMapToStructure(map));
+ }
+
+ /**
+ * Convert protobuf map with {@link com.google.protobuf.Value} to OpenFeature
+ * map.
+ */
+ public static Structure convertProtobufMapToStructure(Map map) {
+ Map values = new HashMap<>();
+
+ map.keySet().forEach((String key) -> {
+ com.google.protobuf.Value value = map.get(key);
+ values.put(key, convertAny(value));
+ });
+ return new MutableStructure(values);
+ }
+
+ /**
+ * Convert OpenFeature list to protobuf {@link com.google.protobuf.Value}.
+ */
+ public static com.google.protobuf.Value convertList(List values) {
+ ListValue list = ListValue.newBuilder()
+ .addAllValues(values.stream()
+ .map(v -> convertAny(v)).collect(Collectors.toList()))
+ .build();
+ return com.google.protobuf.Value.newBuilder().setListValue(list).build();
+ }
+
+ /**
+ * Convert protobuf list to OpenFeature {@link com.google.protobuf.Value}.
+ */
+ public static Value convertList(ListValue protobuf) {
+ return new Value(protobuf.getValuesList().stream().map(p -> convertAny(p)).collect(Collectors.toList()));
+ }
+
+ /**
+ * Convert OpenFeature {@link Value} to protobuf
+ * {@link com.google.protobuf.Value}.
+ */
+ public static com.google.protobuf.Value convertPrimitive(Value value) {
+ com.google.protobuf.Value.Builder builder = com.google.protobuf.Value.newBuilder();
+
+ if (value.isBoolean()) {
+ builder.setBoolValue(value.asBoolean());
+ } else if (value.isString()) {
+ builder.setStringValue(value.asString());
+ } else if (value.isNumber()) {
+ builder.setNumberValue(value.asDouble());
+ } else {
+ builder.setNullValue(NullValue.NULL_VALUE);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Convert protobuf {@link com.google.protobuf.Value} to OpenFeature
+ * {@link Value}.
+ */
+ public static Value convertPrimitive(com.google.protobuf.Value protobuf) {
+ final Value value;
+ if (protobuf.hasBoolValue()) {
+ value = new Value(protobuf.getBoolValue());
+ } else if (protobuf.hasStringValue()) {
+ value = new Value(protobuf.getStringValue());
+ } else if (protobuf.hasNumberValue()) {
+ value = new Value(protobuf.getNumberValue());
+ } else {
+ value = new Value();
+ }
+
+ return value;
+ }
+
+ /**
+ * Get the specified protobuf field from the message.
+ *
+ * @param type
+ * @param message protobuf message
+ * @param name field name
+ * @return field value
+ */
+ public static T getField(Message message, String name) {
+ return (T) message.getField(getFieldDescriptor(message, name));
+ }
+
+ /**
+ * Get the specified protobuf field descriptor from the message.
+ *
+ * @param message protobuf message
+ * @param name field name
+ * @return field descriptor
+ */
+ public static Descriptors.FieldDescriptor getFieldDescriptor(Message message, String name) {
+ return message.getDescriptorForType().findFieldByName(name);
+ }
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/Constants.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/Constants.java
new file mode 100644
index 000000000..d6ba68efd
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/Constants.java
@@ -0,0 +1,10 @@
+package dev.openfeature.contrib.providers.flagd.resolver.grpc;
+
+/**
+ * Constants for evaluation proto.
+ */
+public class Constants {
+ public static final String CONFIGURATION_CHANGE = "configuration_change";
+ public static final String PROVIDER_READY = "provider_ready";
+ public static final String FLAGS_KEY = "flags";
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
index ed279da8e..927fcebdc 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
@@ -20,34 +20,30 @@
@Slf4j
@SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects")
class EventStreamObserver implements StreamObserver {
- private final BiConsumer> stateConsumer;
+ private final BiConsumer> onConnectionEvent;
private final Object sync;
private final Cache cache;
- private static final String CONFIGURATION_CHANGE = "configuration_change";
- private static final String PROVIDER_READY = "provider_ready";
- static final String FLAGS_KEY = "flags";
-
/**
* Create a gRPC stream that get notified about flag changes.
*
- * @param sync synchronization object from caller
- * @param cache cache to update
- * @param stateConsumer lambda to call for setting the state
+ * @param sync synchronization object from caller
+ * @param cache cache to update
+ * @param onConnectionEvent lambda to call to handle the response
*/
- EventStreamObserver(Object sync, Cache cache, BiConsumer> stateConsumer) {
+ EventStreamObserver(Object sync, Cache cache, BiConsumer> onConnectionEvent) {
this.sync = sync;
this.cache = cache;
- this.stateConsumer = stateConsumer;
+ this.onConnectionEvent = onConnectionEvent;
}
@Override
public void onNext(EventStreamResponse value) {
switch (value.getType()) {
- case CONFIGURATION_CHANGE:
+ case Constants.CONFIGURATION_CHANGE:
this.handleConfigurationChangeEvent(value);
break;
- case PROVIDER_READY:
+ case Constants.PROVIDER_READY:
this.handleProviderReadyEvent();
break;
default:
@@ -61,7 +57,7 @@ public void onError(Throwable t) {
if (this.cache.getEnabled()) {
this.cache.clear();
}
- this.stateConsumer.accept(false, Collections.emptyList());
+ this.onConnectionEvent.accept(false, Collections.emptyList());
// handle last call of this stream
handleEndOfStream();
@@ -72,7 +68,7 @@ public void onCompleted() {
if (this.cache.getEnabled()) {
this.cache.clear();
}
- this.stateConsumer.accept(false, Collections.emptyList());
+ this.onConnectionEvent.accept(false, Collections.emptyList());
// handle last call of this stream
handleEndOfStream();
@@ -83,7 +79,7 @@ private void handleConfigurationChangeEvent(EventStreamResponse value) {
boolean cachingEnabled = this.cache.getEnabled();
Map data = value.getData().getFieldsMap();
- Value flagsValue = data.get(FLAGS_KEY);
+ Value flagsValue = data.get(Constants.FLAGS_KEY);
if (flagsValue == null) {
if (cachingEnabled) {
this.cache.clear();
@@ -99,11 +95,11 @@ private void handleConfigurationChangeEvent(EventStreamResponse value) {
}
}
- this.stateConsumer.accept(true, changedFlags);
+ this.onConnectionEvent.accept(true, changedFlags);
}
private void handleProviderReadyEvent() {
- this.stateConsumer.accept(true, Collections.emptyList());
+ this.onConnectionEvent.accept(true, Collections.emptyList());
if (this.cache.getEnabled()) {
this.cache.clear();
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java
index 563bad739..4b2563afe 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java
@@ -4,11 +4,12 @@
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
-import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import java.util.function.Supplier;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelBuilder;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
import dev.openfeature.contrib.providers.flagd.resolver.common.Util;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamRequest;
@@ -37,7 +38,7 @@ public class GrpcConnector {
private final long deadline;
private final Cache cache;
- private final BiConsumer> stateConsumer;
+ private final Consumer onConnectionEvent;
private final Supplier connectedSupplier;
private int eventStreamAttempt = 1;
@@ -48,23 +49,23 @@ public class GrpcConnector {
/**
* GrpcConnector creates an abstraction over gRPC communication.
- *
- * @param options options to build the gRPC channel.
- * @param cache cache to use.
- * @param stateConsumer lambda to call for setting the state.
+ *
+ * @param options flagd options
+ * @param cache cache to use
+ * @param connectedSupplier lambda providing current connection status from caller
+ * @param onConnectionEvent lambda which handles changes in the connection/stream
*/
public GrpcConnector(final FlagdOptions options, final Cache cache, final Supplier connectedSupplier,
- BiConsumer> stateConsumer) {
+ Consumer onConnectionEvent) {
this.channel = ChannelBuilder.nettyChannel(options);
this.serviceStub = ServiceGrpc.newStub(channel);
this.serviceBlockingStub = ServiceGrpc.newBlockingStub(channel);
-
this.maxEventStreamRetries = options.getMaxEventStreamRetries();
this.startEventStreamRetryBackoff = options.getRetryBackoffMs();
this.eventStreamRetryBackoff = options.getRetryBackoffMs();
this.deadline = options.getDeadline();
this.cache = cache;
- this.stateConsumer = stateConsumer;
+ this.onConnectionEvent = onConnectionEvent;
this.connectedSupplier = connectedSupplier;
}
@@ -104,7 +105,7 @@ public void shutdown() throws Exception {
this.channel.awaitTermination(this.deadline, TimeUnit.MILLISECONDS);
log.warn(String.format("Unable to shut down channel by %d deadline", this.deadline));
}
- this.stateConsumer.accept(false, Collections.emptyList());
+ this.onConnectionEvent.accept(new ConnectionEvent(false));
}
}
@@ -124,7 +125,7 @@ public ServiceGrpc.ServiceBlockingStub getResolver() {
private void observeEventStream() {
while (this.eventStreamAttempt <= this.maxEventStreamRetries) {
final StreamObserver responseObserver = new EventStreamObserver(sync, this.cache,
- this::grpcStateConsumer);
+ this::onConnectionEvent);
this.serviceStub.eventStream(EventStreamRequest.getDefaultInstance(), responseObserver);
try {
@@ -155,16 +156,16 @@ private void observeEventStream() {
}
log.error("failed to connect to event stream, exhausted retries");
- this.grpcStateConsumer(false, null);
+ this.onConnectionEvent(false, Collections.emptyList());
}
- private void grpcStateConsumer(final boolean connected, final List changedFlags) {
+ private void onConnectionEvent(final boolean connected, final List changedFlags) {
// reset reconnection states
if (connected) {
this.eventStreamAttempt = 1;
this.eventStreamRetryBackoff = this.startEventStreamRetryBackoff;
}
// chain to initiator
- this.stateConsumer.accept(connected, changedFlags);
+ this.onConnectionEvent.accept(new ConnectionEvent(connected, changedFlags));
}
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java
index 7226d7257..9fcede67e 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java
@@ -1,22 +1,22 @@
package dev.openfeature.contrib.providers.flagd.resolver.grpc;
-import java.util.HashMap;
-import java.util.List;
+import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.convertContext;
+import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.convertObjectResponse;
+import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.getField;
+import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.getFieldDescriptor;
+
import java.util.Map;
-import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import com.google.protobuf.Descriptors;
-import com.google.protobuf.ListValue;
import com.google.protobuf.Message;
-import com.google.protobuf.NullValue;
import com.google.protobuf.Struct;
import dev.openfeature.contrib.providers.flagd.Config;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.strategy.ResolveFactory;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.strategy.ResolveStrategy;
@@ -27,7 +27,6 @@
import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveStringRequest;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableMetadata;
-import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
@@ -40,7 +39,8 @@
import io.grpc.StatusRuntimeException;
/**
- * FlagResolution resolves flags from flagd.
+ * Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.evaluation.v1.
+ * Flags are evaluated remotely.
*/
@SuppressWarnings("PMD.TooManyStaticImports")
@SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects")
@@ -52,20 +52,20 @@ public final class GrpcResolver implements Resolver {
private final Supplier connectedSupplier;
/**
- * Initialize Grpc resolver.
- *
- * @param options flagd options.
- * @param cache cache to use.
- * @param connectedSupplier lambda to call for getting the state.
- * @param onResolverConnectionChanged lambda to communicate back the state.
+ * Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.evaluation.v1.
+ * Flags are evaluated remotely.
+ *
+ * @param options flagd options
+ * @param cache cache to use
+ * @param connectedSupplier lambda providing current connection status from caller
+ * @param onConnectionEvent lambda which handles changes in the connection/stream
*/
public GrpcResolver(final FlagdOptions options, final Cache cache, final Supplier connectedSupplier,
- final BiConsumer> onResolverConnectionChanged) {
+ final Consumer onConnectionEvent) {
this.cache = cache;
this.connectedSupplier = connectedSupplier;
-
this.strategy = ResolveFactory.getStrategy(options);
- this.connector = new GrpcConnector(options, cache, connectedSupplier, onResolverConnectionChanged);
+ this.connector = new GrpcConnector(options, cache, connectedSupplier, onConnectionEvent);
}
/**
@@ -200,145 +200,6 @@ private Boolean cacheAvailable() {
return this.cache.getEnabled() && this.connectedSupplier.get();
}
- /**
- * Recursively convert protobuf structure to openfeature value.
- */
- private static Value convertObjectResponse(Struct protobuf) {
- return convertProtobufMap(protobuf.getFieldsMap());
- }
-
- /**
- * Recursively convert the Evaluation context to a protobuf structure.
- */
- private static Struct convertContext(EvaluationContext ctx) {
- Map ctxMap = ctx.asMap();
- // asMap() does not provide explicitly set targeting key (ex:- new
- // ImmutableContext("TargetingKey") ).
- // Hence, we add this explicitly here for targeting rule processing.
- ctxMap.put("targetingKey", new Value(ctx.getTargetingKey()));
-
- return convertMap(ctxMap).getStructValue();
- }
-
- /**
- * Convert any openfeature value to a protobuf value.
- */
- private static com.google.protobuf.Value convertAny(Value value) {
- if (value.isList()) {
- return convertList(value.asList());
- } else if (value.isStructure()) {
- return convertMap(value.asStructure().asMap());
- } else {
- return convertPrimitive(value);
- }
- }
-
- /**
- * Convert any protobuf value to {@link Value}.
- */
- private static Value convertAny(com.google.protobuf.Value protobuf) {
- if (protobuf.hasListValue()) {
- return convertList(protobuf.getListValue());
- } else if (protobuf.hasStructValue()) {
- return convertProtobufMap(protobuf.getStructValue().getFieldsMap());
- } else {
- return convertPrimitive(protobuf);
- }
- }
-
- /**
- * Convert OpenFeature map to protobuf {@link com.google.protobuf.Value}.
- */
- private static com.google.protobuf.Value convertMap(Map map) {
- Map values = new HashMap<>();
-
- map.keySet().forEach((String key) -> {
- Value value = map.get(key);
- values.put(key, convertAny(value));
- });
- Struct struct = Struct.newBuilder()
- .putAllFields(values).build();
- return com.google.protobuf.Value.newBuilder().setStructValue(struct).build();
- }
-
- /**
- * Convert protobuf map with {@link com.google.protobuf.Value} to OpenFeature
- * map.
- */
- private static Value convertProtobufMap(Map map) {
- Map values = new HashMap<>();
-
- map.keySet().forEach((String key) -> {
- com.google.protobuf.Value value = map.get(key);
- values.put(key, convertAny(value));
- });
- return new Value(new MutableStructure(values));
- }
-
- /**
- * Convert OpenFeature list to protobuf {@link com.google.protobuf.Value}.
- */
- private static com.google.protobuf.Value convertList(List values) {
- ListValue list = ListValue.newBuilder()
- .addAllValues(values.stream()
- .map(v -> convertAny(v)).collect(Collectors.toList()))
- .build();
- return com.google.protobuf.Value.newBuilder().setListValue(list).build();
- }
-
- /**
- * Convert protobuf list to OpenFeature {@link com.google.protobuf.Value}.
- */
- private static Value convertList(ListValue protobuf) {
- return new Value(protobuf.getValuesList().stream().map(p -> convertAny(p)).collect(Collectors.toList()));
- }
-
- /**
- * Convert OpenFeature {@link Value} to protobuf
- * {@link com.google.protobuf.Value}.
- */
- private static com.google.protobuf.Value convertPrimitive(Value value) {
- com.google.protobuf.Value.Builder builder = com.google.protobuf.Value.newBuilder();
-
- if (value.isBoolean()) {
- builder.setBoolValue(value.asBoolean());
- } else if (value.isString()) {
- builder.setStringValue(value.asString());
- } else if (value.isNumber()) {
- builder.setNumberValue(value.asDouble());
- } else {
- builder.setNullValue(NullValue.NULL_VALUE);
- }
- return builder.build();
- }
-
- /**
- * Convert protobuf {@link com.google.protobuf.Value} to OpenFeature
- * {@link Value}.
- */
- private static Value convertPrimitive(com.google.protobuf.Value protobuf) {
- final Value value;
- if (protobuf.hasBoolValue()) {
- value = new Value(protobuf.getBoolValue());
- } else if (protobuf.hasStringValue()) {
- value = new Value(protobuf.getStringValue());
- } else if (protobuf.hasNumberValue()) {
- value = new Value(protobuf.getNumberValue());
- } else {
- value = new Value();
- }
-
- return value;
- }
-
- private static T getField(Message message, String name) {
- return (T) message.getField(getFieldDescriptor(message, name));
- }
-
- private static Descriptors.FieldDescriptor getFieldDescriptor(Message message, String name) {
- return message.getDescriptorForType().findFieldByName(name);
- }
-
private static ImmutableMetadata metadataFromResponse(Message response) {
final Object metadata = response.getField(getFieldDescriptor(response, Config.METADATA_FIELD));
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 06e641736..39c77f01b 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
@@ -2,12 +2,12 @@
import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING;
-import java.util.List;
-import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import java.util.function.Supplier;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
import dev.openfeature.contrib.providers.flagd.resolver.common.Util;
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore;
@@ -29,30 +29,35 @@
import lombok.extern.slf4j.Slf4j;
/**
- * flagd in-process resolver. Resolves feature flags in-process. Flags are
- * retrieved from {@link Storage}, where the
- * {@link Storage} maintain flag configurations obtained from known source.
+ * Resolves flag values using
+ * https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1.
+ * Flags are evaluated locally.
*/
@Slf4j
public class InProcessResolver implements Resolver {
private final Storage flagStore;
- private final BiConsumer> onResolverConnectionChanged;
+ private final Consumer onConnectionEvent;
private final Operator operator;
private final long deadline;
private final ImmutableMetadata metadata;
private final Supplier connectedSupplier;
/**
- * Initialize an in-process resolver.
- * @param options flagd options
- * @param connectedSupplier supplier for connection state
- * @param onResolverConnectionChanged handler for connection change
+ * Resolves flag values using
+ * https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1.
+ * Flags are evaluated locally.
+ *
+ * @param options flagd options
+ * @param connectedSupplier lambda providing current connection status from
+ * caller
+ * @param onConnectionEvent lambda which handles changes in the
+ * connection/stream
*/
public InProcessResolver(FlagdOptions options, final Supplier connectedSupplier,
- BiConsumer> onResolverConnectionChanged) {
+ Consumer onConnectionEvent) {
this.flagStore = new FlagStore(getConnector(options));
this.deadline = options.getDeadline();
- this.onResolverConnectionChanged = onResolverConnectionChanged;
+ this.onConnectionEvent = onConnectionEvent;
this.operator = new Operator();
this.connectedSupplier = connectedSupplier;
this.metadata = options.getSelector() == null ? null
@@ -72,10 +77,11 @@ public void init() throws Exception {
final StorageStateChange storageStateChange = flagStore.getStateQueue().take();
switch (storageStateChange.getStorageState()) {
case OK:
- onResolverConnectionChanged.accept(true, storageStateChange.getChangedFlagsKeys());
+ onConnectionEvent.accept(new ConnectionEvent(true, storageStateChange.getChangedFlagsKeys(),
+ storageStateChange.getSyncMetadata()));
break;
case ERROR:
- onResolverConnectionChanged.accept(false, null);
+ onConnectionEvent.accept(new ConnectionEvent(false));
break;
default:
log.info(String.format("Storage emitted unhandled status: %s",
@@ -101,7 +107,7 @@ public void init() throws Exception {
*/
public void shutdown() throws InterruptedException {
flagStore.shutdown();
- onResolverConnectionChanged.accept(false, null);
+ onConnectionEvent.accept(new ConnectionEvent(false));
}
/**
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java
index 3b4c03113..a4581f338 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java
@@ -1,11 +1,8 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;
-import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
-import dev.openfeature.contrib.providers.flagd.resolver.process.model.FlagParser;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import lombok.extern.slf4j.Slf4j;
+import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.convertProtobufMapToStructure;
+
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -17,6 +14,14 @@
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import java.util.stream.Collectors;
+import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
+import dev.openfeature.contrib.providers.flagd.resolver.process.model.FlagParser;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
+import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import lombok.extern.slf4j.Slf4j;
+
/**
* Feature flag storage.
*/
@@ -94,15 +99,17 @@ public BlockingQueue getStateQueue() {
}
private void streamerListener(final Connector connector) throws InterruptedException {
- final BlockingQueue streamPayloads = connector.getStream();
+ final BlockingQueue streamPayloads = connector.getStream();
while (!shutdown.get()) {
- final StreamPayload take = streamPayloads.take();
- switch (take.getType()) {
+ final QueuePayload payload = streamPayloads.take();
+ switch (payload.getType()) {
case DATA:
try {
List changedFlagsKeys;
- Map flagMap = FlagParser.parseString(take.getData(), throwIfInvalid);
+ Map flagMap = FlagParser.parseString(payload.getFlagData(),
+ throwIfInvalid);
+ Map metadata = parseSyncMetadata(payload.getMetadataResponse());
writeLock.lock();
try {
changedFlagsKeys = getChangedFlagsKeys(flagMap);
@@ -111,7 +118,8 @@ private void streamerListener(final Connector connector) throws InterruptedExcep
} finally {
writeLock.unlock();
}
- if (!stateBlockingQueue.offer(new StorageStateChange(StorageState.OK, changedFlagsKeys))) {
+ if (!stateBlockingQueue
+ .offer(new StorageStateChange(StorageState.OK, changedFlagsKeys, metadata))) {
log.warn("Failed to convey OK satus, queue is full");
}
} catch (Throwable e) {
@@ -128,13 +136,23 @@ private void streamerListener(final Connector connector) throws InterruptedExcep
}
break;
default:
- log.info(String.format("Payload with unknown type: %s", take.getType()));
+ log.info(String.format("Payload with unknown type: %s", payload.getType()));
}
}
log.info("Shutting down store stream listener");
}
+ private Map parseSyncMetadata(GetMetadataResponse metadataResponse) {
+ try {
+ return convertProtobufMapToStructure(metadataResponse.getMetadata().getFieldsMap())
+ .asObjectMap();
+ } catch (Exception exception) {
+ log.error("Failed to parse metadataResponse, provider metadata may not be up-to-date");
+ }
+ return Collections.emptyMap();
+ }
+
private List getChangedFlagsKeys(Map newFlags) {
Map changedFlags = new HashMap<>();
Map addedFeatureFlags = new HashMap<>();
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java
index cf85b0432..042bd082b 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java
@@ -1,13 +1,13 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
/**
* Represents a change in the stored flags.
*/
@@ -17,14 +17,39 @@
public class StorageStateChange {
private final StorageState storageState;
private final List changedFlagsKeys;
+ private final Map syncMetadata;
+
+ /**
+ * Construct a new StorageStateChange.
+ * @param storageState state of the storage
+ * @param changedFlagsKeys flags changed
+ * @param syncMetadata possibly updated metadata
+ */
+ public StorageStateChange(StorageState storageState, List changedFlagsKeys,
+ Map syncMetadata) {
+ this.storageState = storageState;
+ this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys);
+ this.syncMetadata = Collections.unmodifiableMap(syncMetadata);
+ }
+ /**
+ * Construct a new StorageStateChange.
+ * @param storageState state of the storage
+ * @param changedFlagsKeys flags changed
+ */
public StorageStateChange(StorageState storageState, List changedFlagsKeys) {
this.storageState = storageState;
- this.changedFlagsKeys = new ArrayList<>(changedFlagsKeys);
+ this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys);
+ this.syncMetadata = Collections.emptyMap();
}
+ /**
+ * Construct a new StorageStateChange.
+ * @param storageState state of the storage
+ */
public StorageStateChange(StorageState storageState) {
this.storageState = storageState;
this.changedFlagsKeys = Collections.emptyList();
+ this.syncMetadata = Collections.emptyMap();
}
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/Connector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/Connector.java
index 66ebf2c90..1a00737b5 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/Connector.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/Connector.java
@@ -4,12 +4,12 @@
/**
* Contract of the in-process storage connector. Connectors are responsible to stream flag configurations in
- * {@link StreamPayload} format.
+ * {@link QueuePayload} format.
*/
public interface Connector {
void init() throws Exception;
- BlockingQueue getStream();
+ BlockingQueue getStream();
void shutdown() throws InterruptedException;
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueuePayload.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueuePayload.java
new file mode 100644
index 000000000..e9983a42d
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueuePayload.java
@@ -0,0 +1,20 @@
+package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector;
+
+import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Payload emitted by a {@link Connector}.
+ */
+@AllArgsConstructor
+@Getter
+public class QueuePayload {
+ private final QueuePayloadType type;
+ private final String flagData;
+ private final GetMetadataResponse metadataResponse;
+
+ public QueuePayload(QueuePayloadType type, String flagData) {
+ this(type, flagData, GetMetadataResponse.getDefaultInstance());
+ }
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/StreamPayloadType.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueuePayloadType.java
similarity index 83%
rename from providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/StreamPayloadType.java
rename to providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueuePayloadType.java
index dad6ba80d..4839dab51 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/StreamPayloadType.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueuePayloadType.java
@@ -3,7 +3,7 @@
/**
* Payload type emitted by {@link Connector}.
*/
-public enum StreamPayloadType {
+public enum QueuePayloadType {
DATA,
ERROR
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/StreamPayload.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/StreamPayload.java
deleted file mode 100644
index f2afc34b5..000000000
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/StreamPayload.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-/**
- * Payload emitted by a {@link Connector}.
- */
-@AllArgsConstructor
-@Getter
-public class StreamPayload {
- private final StreamPayloadType type;
- private final String data;
-}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnector.java
index 775b5a453..a60c58be2 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnector.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnector.java
@@ -1,11 +1,5 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import lombok.extern.slf4j.Slf4j;
-
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -14,6 +8,12 @@
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import lombok.extern.slf4j.Slf4j;
+
/**
* File connector reads flag configurations from a given file, polls for changes and expose the content through
* {@code Connector} contract.
@@ -28,7 +28,7 @@ public class FileConnector implements Connector {
private static final String OFFER_WARN = "Unable to offer file content to queue: queue is full";
private final String flagSourcePath;
- private final BlockingQueue queue = new LinkedBlockingQueue<>(1);
+ private final BlockingQueue queue = new LinkedBlockingQueue<>(1);
private boolean shutdown = false;
public FileConnector(final String flagSourcePath) {
@@ -45,7 +45,7 @@ public void init() throws IOException {
// initial read
String flagData = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
- if (!queue.offer(new StreamPayload(StreamPayloadType.DATA, flagData))) {
+ if (!queue.offer(new QueuePayload(QueuePayloadType.DATA, flagData))) {
log.warn(OFFER_WARN);
}
@@ -58,7 +58,7 @@ public void init() throws IOException {
if (currentTS > lastTS) {
lastTS = currentTS;
flagData = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
- if (!queue.offer(new StreamPayload(StreamPayloadType.DATA, flagData))) {
+ if (!queue.offer(new QueuePayload(QueuePayloadType.DATA, flagData))) {
log.warn(OFFER_WARN);
}
}
@@ -72,7 +72,7 @@ public void init() throws IOException {
Thread.currentThread().interrupt();
} catch (Throwable t) {
log.error("Error from file connector. File connector will exit", t);
- if (!queue.offer(new StreamPayload(StreamPayloadType.ERROR, t.toString()))) {
+ if (!queue.offer(new QueuePayload(QueuePayloadType.ERROR, t.toString(), null))) {
log.warn(OFFER_WARN);
}
}
@@ -86,7 +86,7 @@ public void init() throws IOException {
/**
* Expose the queue to fulfil the {@code Connector} contract.
*/
- public BlockingQueue getStream() {
+ public BlockingQueue getStream() {
return queue;
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java
index 4e28f8bdf..769c70a30 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java
@@ -1,24 +1,29 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.grpc;
+import java.util.Random;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelBuilder;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType;
import dev.openfeature.flagd.grpc.sync.FlagSyncServiceGrpc;
+import dev.openfeature.flagd.grpc.sync.FlagSyncServiceGrpc.FlagSyncServiceBlockingStub;
import dev.openfeature.flagd.grpc.sync.FlagSyncServiceGrpc.FlagSyncServiceStub;
+import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataRequest;
+import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
import dev.openfeature.flagd.grpc.sync.Sync.SyncFlagsRequest;
import dev.openfeature.flagd.grpc.sync.Sync.SyncFlagsResponse;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.grpc.Context;
+import io.grpc.Context.CancellableContext;
import io.grpc.ManagedChannel;
import lombok.extern.slf4j.Slf4j;
-import java.util.Random;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
/**
* Implements the {@link Connector} contract and emit flags obtained from flagd
* sync gRPC contract.
@@ -28,17 +33,15 @@
"EI_EXPOSE_REP" }, justification = "Random is used to generate a variation & flag configurations require exposing")
public class GrpcStreamConnector implements Connector {
private static final Random RANDOM = new Random();
-
private static final int INIT_BACK_OFF = 2 * 1000;
private static final int MAX_BACK_OFF = 120 * 1000;
-
private static final int QUEUE_SIZE = 5;
private final AtomicBoolean shutdown = new AtomicBoolean(false);
- private final BlockingQueue blockingQueue = new LinkedBlockingQueue<>(QUEUE_SIZE);
-
+ private final BlockingQueue blockingQueue = new LinkedBlockingQueue<>(QUEUE_SIZE);
private final ManagedChannel channel;
- private final FlagSyncServiceGrpc.FlagSyncServiceStub serviceStub;
+ private final FlagSyncServiceStub serviceStub;
+ private final FlagSyncServiceBlockingStub serviceBlockingStub;
private final int deadline;
private final String selector;
@@ -50,6 +53,7 @@ public class GrpcStreamConnector implements Connector {
public GrpcStreamConnector(final FlagdOptions options) {
channel = ChannelBuilder.nettyChannel(options);
serviceStub = FlagSyncServiceGrpc.newStub(channel);
+ serviceBlockingStub = FlagSyncServiceGrpc.newBlockingStub(channel);
deadline = options.getDeadline();
selector = options.getSelector();
}
@@ -60,13 +64,7 @@ public GrpcStreamConnector(final FlagdOptions options) {
public void init() {
Thread listener = new Thread(() -> {
try {
- final SyncFlagsRequest.Builder requestBuilder = SyncFlagsRequest.newBuilder();
-
- if (selector != null) {
- requestBuilder.setSelector(selector);
- }
-
- observeEventStream(blockingQueue, shutdown, serviceStub, requestBuilder.build());
+ observeEventStream(blockingQueue, shutdown, serviceStub, serviceBlockingStub, selector, deadline);
} catch (InterruptedException e) {
log.warn("gRPC event stream interrupted, flag configurations are stale", e);
Thread.currentThread().interrupt();
@@ -80,7 +78,7 @@ public void init() {
/**
* Get blocking queue to obtain payloads exposed by this connector.
*/
- public BlockingQueue getStream() {
+ public BlockingQueue getStream() {
return blockingQueue;
}
@@ -111,10 +109,12 @@ public void shutdown() throws InterruptedException {
/**
* Contains blocking calls, to be used concurrently.
*/
- static void observeEventStream(final BlockingQueue writeTo,
+ static void observeEventStream(final BlockingQueue writeTo,
final AtomicBoolean shutdown,
final FlagSyncServiceStub serviceStub,
- final SyncFlagsRequest request)
+ final FlagSyncServiceBlockingStub serviceBlockingStub,
+ final String selector,
+ final int deadline)
throws InterruptedException {
final BlockingQueue streamReceiver = new LinkedBlockingQueue<>(QUEUE_SIZE);
@@ -123,40 +123,66 @@ static void observeEventStream(final BlockingQueue writeTo,
log.info("Initializing sync stream observer");
while (!shutdown.get()) {
+ writeTo.clear();
+ Exception metadataException = null;
log.debug("Initializing sync stream request");
- serviceStub.syncFlags(request, new GrpcStreamHandler(streamReceiver));
+ final SyncFlagsRequest.Builder syncRequest = SyncFlagsRequest.newBuilder();
+ final GetMetadataRequest.Builder metadataRequest = GetMetadataRequest.newBuilder();
+ GetMetadataResponse metadataResponse = GetMetadataResponse.getDefaultInstance();
- while (!shutdown.get()) {
- final GrpcResponseModel response = streamReceiver.take();
+ if (selector != null) {
+ syncRequest.setSelector(selector);
+ }
- if (response.isComplete()) {
- log.info("Sync stream completed");
- // The stream is complete, this isn't really an error but we should try to
- // reconnect
- break;
+ try (CancellableContext context = Context.current().withCancellation()) {
+ serviceStub.syncFlags(syncRequest.build(), new GrpcStreamHandler(streamReceiver));
+ try {
+ metadataResponse = serviceBlockingStub.withDeadlineAfter(deadline, TimeUnit.MILLISECONDS)
+ .getMetadata(metadataRequest.build());
+ } catch (Exception e) {
+ // the chances this call fails but the syncRequest does not are slim
+ // it could be that the server doesn't implement this RPC
+ // instead of logging and throwing here, retain the exception and handle in the
+ // stream logic below
+ metadataException = e;
}
- if (response.getError() != null) {
- log.error(String.format("Error from grpc connection, retrying in %dms", retryDelay),
- response.getError());
+ while (!shutdown.get()) {
+ final GrpcResponseModel response = streamReceiver.take();
+
+ if (response.isComplete()) {
+ log.info("Sync stream completed");
+ // The stream is complete, this isn't really an error but we should try to
+ // reconnect
+ break;
+ }
+
+ if (response.getError() != null || metadataException != null) {
+ log.error(String.format("Error from initializing stream or metadata, retrying in %dms",
+ retryDelay), response.getError());
+
+ if (!writeTo.offer(
+ new QueuePayload(QueuePayloadType.ERROR, "Error from stream or metadata",
+ metadataResponse))) {
+ log.error("Failed to convey ERROR status, queue is full");
+ }
+ // close the context to cancel the stream in case just the metadata call failed
+ context.cancel(metadataException);
+ break;
+ }
+
+ final SyncFlagsResponse flagsResponse = response.getSyncFlagsResponse();
+ String data = flagsResponse.getFlagConfiguration();
+ log.debug("Got stream response: " + data);
if (!writeTo.offer(
- new StreamPayload(StreamPayloadType.ERROR, "Error from stream connection, retrying"))) {
- log.error("Failed to convey ERROR status, queue is full");
+ new QueuePayload(QueuePayloadType.DATA, data, metadataResponse))) {
+ log.error("Stream writing failed");
}
- break;
- }
- final SyncFlagsResponse flagsResponse = response.getSyncFlagsResponse();
- String data = flagsResponse.getFlagConfiguration();
- log.debug("Got stream response: " + data);
- if (!writeTo.offer(
- new StreamPayload(StreamPayloadType.DATA, data))) {
- log.error("Stream writing failed");
+ // reset retry delay if we succeeded in a retry attempt
+ retryDelay = INIT_BACK_OFF;
}
-
- // reset retry delay if we succeeded in a retry attempt
- retryDelay = INIT_BACK_OFF;
}
// check for shutdown and avoid sleep
@@ -166,7 +192,7 @@ static void observeEventStream(final BlockingQueue writeTo,
}
// busy wait till next attempt
- log.warn(String.format("Stream failed, retrying in %dms", retryDelay));
+ log.debug(String.format("Stream failed, retrying in %dms", retryDelay));
Thread.sleep(retryDelay + RANDOM.nextInt(INIT_BACK_OFF));
if (retryDelay < MAX_BACK_OFF) {
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 f1318ac4b..269c14d9d 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
@@ -11,6 +11,7 @@
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -20,23 +21,27 @@
import java.lang.reflect.Field;
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.concurrent.TimeUnit;
import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.concurrent.LinkedBlockingQueue;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import com.google.protobuf.Struct;
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcConnector;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
@@ -60,6 +65,7 @@
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.OpenFeatureAPI;
+import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.ProviderState;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
@@ -67,846 +73,939 @@
import io.cucumber.java.AfterAll;
import io.grpc.Channel;
import io.grpc.Deadline;
+import lombok.val;
class FlagdProviderTest {
- private static final String FLAG_KEY = "some-key";
- private static final String FLAG_KEY_BOOLEAN = "some-key-boolean";
- private static final String FLAG_KEY_INTEGER = "some-key-integer";
- private static final String FLAG_KEY_DOUBLE = "some-key-double";
- private static final String FLAG_KEY_STRING = "some-key-string";
- private static final String FLAG_KEY_OBJECT = "some-key-object";
- private static final String BOOL_VARIANT = "on";
- private static final String DOUBLE_VARIANT = "half";
- private static final String INT_VARIANT = "one-hundred";
- private static final String STRING_VARIANT = "greeting";
- private static final String OBJECT_VARIANT = "obj";
- private static final Reason DEFAULT = Reason.DEFAULT;
- private static final Integer INT_VALUE = 100;
- private static final Double DOUBLE_VALUE = .5d;
- private static final String INNER_STRUCT_KEY = "inner_key";
- private static final String INNER_STRUCT_VALUE = "inner_value";
- private static final com.google.protobuf.Struct PROTOBUF_STRUCTURE_VALUE = Struct.newBuilder()
- .putFields(INNER_STRUCT_KEY,
- com.google.protobuf.Value.newBuilder().setStringValue(INNER_STRUCT_VALUE).build())
- .build();
- private static final String STRING_VALUE = "hi!";
-
- private static OpenFeatureAPI api;
-
- @BeforeAll
- public static void init() {
- api = OpenFeatureAPI.getInstance();
- }
-
- @AfterAll
- public static void cleanUp() {
- api.shutdown();
- }
-
- @Test
- void resolvers_call_grpc_service_and_return_details() {
- ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
- .setValue(true)
- .setVariant(BOOL_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
- .setValue(STRING_VALUE)
- .setVariant(STRING_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
- .setValue(INT_VALUE)
- .setVariant(INT_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
- .setValue(DOUBLE_VALUE)
- .setVariant(DOUBLE_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
- .setValue(PROTOBUF_STRUCTURE_VALUE)
- .setVariant(OBJECT_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
-
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock
- .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))).thenReturn(booleanResponse);
- when(serviceBlockingStubMock
- .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))).thenReturn(floatResponse);
- when(serviceBlockingStubMock
- .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))).thenReturn(intResponse);
- when(serviceBlockingStubMock
- .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))).thenReturn(stringResponse);
- when(serviceBlockingStubMock
- .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))).thenReturn(objectResponse);
-
- GrpcConnector grpc = mock(GrpcConnector.class);
- when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
-
- OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
-
- FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
- assertTrue(booleanDetails.getValue());
- assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
- assertEquals(DEFAULT.toString(), booleanDetails.getReason());
-
- FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
- assertEquals(STRING_VALUE, stringDetails.getValue());
- assertEquals(STRING_VARIANT, stringDetails.getVariant());
- assertEquals(DEFAULT.toString(), stringDetails.getReason());
-
- FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
- assertEquals(INT_VALUE, intDetails.getValue());
- assertEquals(INT_VARIANT, intDetails.getVariant());
- assertEquals(DEFAULT.toString(), intDetails.getReason());
-
- FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
- assertEquals(DOUBLE_VALUE, floatDetails.getValue());
- assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
- assertEquals(DEFAULT.toString(), floatDetails.getReason());
-
- FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
- assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
- .asMap().get(INNER_STRUCT_KEY).asString());
- assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
- assertEquals(DEFAULT.toString(), objectDetails.getReason());
- }
-
- @Test
- void zero_value() {
- ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
- .setVariant(BOOL_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
- .setVariant(STRING_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
- .setVariant(INT_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
- .setVariant(DOUBLE_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
- .setVariant(OBJECT_VARIANT)
- .setReason(DEFAULT.toString())
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock
- .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))).thenReturn(booleanResponse);
- when(serviceBlockingStubMock
- .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))).thenReturn(floatResponse);
- when(serviceBlockingStubMock
- .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))).thenReturn(intResponse);
- when(serviceBlockingStubMock
- .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))).thenReturn(stringResponse);
- when(serviceBlockingStubMock
- .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))).thenReturn(objectResponse);
-
- GrpcConnector grpc = mock(GrpcConnector.class);
- when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
-
- OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
-
- FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
- assertEquals(false, booleanDetails.getValue());
- assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
- assertEquals(DEFAULT.toString(), booleanDetails.getReason());
-
- FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
- assertEquals("", stringDetails.getValue());
- assertEquals(STRING_VARIANT, stringDetails.getVariant());
- assertEquals(DEFAULT.toString(), stringDetails.getReason());
-
- FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
- assertEquals(0, intDetails.getValue());
- assertEquals(INT_VARIANT, intDetails.getVariant());
- assertEquals(DEFAULT.toString(), intDetails.getReason());
-
- FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
- assertEquals(0.0, floatDetails.getValue());
- assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
- assertEquals(DEFAULT.toString(), floatDetails.getReason());
-
- FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
- assertEquals(new MutableStructure(), objectDetails.getValue().asObject());
- assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
- assertEquals(DEFAULT.toString(), objectDetails.getReason());
- }
-
- @Test
- void test_metadata_from_grpc_response() {
- // given
- final Map metadataInput = new HashMap<>();
-
- com.google.protobuf.Value scope = com.google.protobuf.Value.newBuilder().setStringValue("flagd-scope").build();
- metadataInput.put("scope", scope);
-
- com.google.protobuf.Value bool = com.google.protobuf.Value.newBuilder().setBoolValue(true).build();
- metadataInput.put("boolean", bool);
-
- com.google.protobuf.Value number = com.google.protobuf.Value.newBuilder().setNumberValue(1).build();
- metadataInput.put("number", number);
-
- final Struct metadataStruct = Struct.newBuilder().putAllFields(metadataInput).build();
-
- ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
- .setValue(true)
- .setVariant(BOOL_VARIANT)
- .setReason(DEFAULT.toString())
- .setMetadata(metadataStruct)
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
-
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))).thenReturn(
- serviceBlockingStubMock);
- when(serviceBlockingStubMock
- .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))).thenReturn(booleanResponse);
-
- GrpcConnector grpc = mock(GrpcConnector.class);
- when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
- OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
-
- // when
- FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
-
- // then
- final ImmutableMetadata metadata = booleanDetails.getFlagMetadata();
-
- assertEquals("flagd-scope", metadata.getString("scope"));
- assertEquals(true, metadata.getBoolean("boolean"));
- assertEquals(1, metadata.getDouble("number"));
- }
-
- @Test
- void resolvers_cache_responses_if_static_and_event_stream_alive() {
- do_resolvers_cache_responses(STATIC_REASON, true, true);
- }
-
- @Test
- void resolvers_should_not_cache_responses_if_not_static() {
- do_resolvers_cache_responses(DEFAULT.toString(), true, false);
- }
-
- @Test
- void resolvers_should_not_cache_responses_if_event_stream_not_alive() {
- do_resolvers_cache_responses(STATIC_REASON, false, false);
- }
-
- @Test
- void context_is_parsed_and_passed_to_grpc_service() {
- final String BOOLEAN_ATTR_KEY = "bool-attr";
- final String INT_ATTR_KEY = "int-attr";
- final String STRING_ATTR_KEY = "string-attr";
- final String STRUCT_ATTR_KEY = "struct-attr";
- final String DOUBLE_ATTR_KEY = "double-attr";
- final String LIST_ATTR_KEY = "list-attr";
- final String STRUCT_ATTR_INNER_KEY = "struct-inner-key";
-
- final Boolean BOOLEAN_ATTR_VALUE = true;
- final int INT_ATTR_VALUE = 1;
- final String STRING_ATTR_VALUE = "str";
- final double DOUBLE_ATTR_VALUE = 0.5d;
- final List LIST_ATTR_VALUE = new ArrayList() {
- {
- add(new Value(1));
- }
- };
- final String STRUCT_ATTR_INNER_VALUE = "struct-inner-value";
- final Structure STRUCT_ATTR_VALUE = new MutableStructure().add(STRUCT_ATTR_INNER_KEY, STRUCT_ATTR_INNER_VALUE);
- final String DEFAULT_STRING = "DEFAULT";
-
- ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
- .setValue(true)
- .setVariant(BOOL_VARIANT)
- .setReason(DEFAULT_STRING.toString())
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock.resolveBoolean(argThat(
- x -> {
- final Struct struct = x.getContext();
- final Map valueMap = struct.getFieldsMap();
-
- return STRING_ATTR_VALUE.equals(valueMap.get(STRING_ATTR_KEY).getStringValue())
- && INT_ATTR_VALUE == valueMap.get(INT_ATTR_KEY).getNumberValue()
- && DOUBLE_ATTR_VALUE == valueMap.get(DOUBLE_ATTR_KEY).getNumberValue()
- && valueMap.get(BOOLEAN_ATTR_KEY).getBoolValue()
- && "MY_TARGETING_KEY".equals(valueMap.get("targetingKey").getStringValue())
- && LIST_ATTR_VALUE.get(0).asInteger() == valueMap.get(LIST_ATTR_KEY).getListValue()
- .getValuesList().get(0).getNumberValue()
- && STRUCT_ATTR_INNER_VALUE.equals(
- valueMap.get(STRUCT_ATTR_KEY).getStructValue().getFieldsMap()
- .get(STRUCT_ATTR_INNER_KEY).getStringValue());
- }))).thenReturn(booleanResponse);
-
- GrpcConnector grpc = mock(GrpcConnector.class);
- when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
-
- OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
-
- final MutableContext context = new MutableContext("MY_TARGETING_KEY");
- context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE);
- context.add(INT_ATTR_KEY, INT_ATTR_VALUE);
- context.add(DOUBLE_ATTR_KEY, DOUBLE_ATTR_VALUE);
- context.add(LIST_ATTR_KEY, LIST_ATTR_VALUE);
- context.add(STRING_ATTR_KEY, STRING_ATTR_VALUE);
- context.add(STRUCT_ATTR_KEY, STRUCT_ATTR_VALUE);
-
- FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, context);
- assertTrue(booleanDetails.getValue());
- assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
- assertEquals(DEFAULT.toString(), booleanDetails.getReason());
- }
-
- // Validates null handling -
- // https://github.com/open-feature/java-sdk-contrib/issues/258
- @Test
- void null_context_handling() {
- // given
- final String flagA = "flagA";
- final boolean defaultVariant = false;
- final boolean expectedVariant = true;
-
- final MutableContext context = new MutableContext();
- context.add("key", (String) null);
-
- final ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
-
- // when
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock.resolveBoolean(any()))
- .thenReturn(ResolveBooleanResponse.newBuilder().setValue(expectedVariant).build());
-
- GrpcConnector grpc = mock(GrpcConnector.class);
- when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
-
- OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
-
- // then
- final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context);
-
- assertNotEquals(evaluation, defaultVariant);
- assertEquals(evaluation, expectedVariant);
- }
-
- @Test
- void reason_mapped_correctly_if_unknown() {
- ResolveBooleanResponse badReasonResponse = ResolveBooleanResponse.newBuilder()
- .setValue(true)
- .setVariant(BOOL_VARIANT)
- .setReason("UNKNOWN") // set an invalid reason string
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock.resolveBoolean(any(ResolveBooleanRequest.class))).thenReturn(badReasonResponse);
-
- GrpcConnector grpc = mock(GrpcConnector.class);
- when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
-
- OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
-
- FlagEvaluationDetails booleanDetails = api.getClient()
- .getBooleanDetails(FLAG_KEY, false, new MutableContext());
- assertEquals(Reason.UNKNOWN.toString(), booleanDetails.getReason()); // reason should be converted to UNKNOWN
- }
-
- @Test
- void invalidate_cache() {
- ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
- .setValue(true)
- .setVariant(BOOL_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
- .setValue(STRING_VALUE)
- .setVariant(STRING_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
- .setValue(INT_VALUE)
- .setVariant(INT_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
- .setValue(DOUBLE_VALUE)
- .setVariant(DOUBLE_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
- .setValue(PROTOBUF_STRUCTURE_VALUE)
- .setVariant(OBJECT_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
- ServiceStub serviceStubMock = mock(ServiceStub.class);
- when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock);
- doNothing().when(serviceStubMock).eventStream(any(), any());
- when(serviceStubMock.withDeadline(any(Deadline.class)))
- .thenReturn(serviceStubMock);
- when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock
- .withDeadline(any(Deadline.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock
- .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))).thenReturn(booleanResponse);
- when(serviceBlockingStubMock
- .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))).thenReturn(floatResponse);
- when(serviceBlockingStubMock
- .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))).thenReturn(intResponse);
- when(serviceBlockingStubMock
- .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))).thenReturn(stringResponse);
- when(serviceBlockingStubMock
- .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))).thenReturn(objectResponse);
-
- GrpcConnector grpc;
- try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) {
- mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class)))
- .thenReturn(serviceBlockingStubMock);
- mockStaticService.when(() -> ServiceGrpc.newStub(any()))
- .thenReturn(serviceStubMock);
-
- final Cache cache = new Cache("lru", 5);
-
- class NoopInitGrpcConnector extends GrpcConnector {
- public NoopInitGrpcConnector(FlagdOptions options, Cache cache, Supplier connectedSupplier, BiConsumer> onResolverConnectionChanged) {
- super(options, cache, connectedSupplier, onResolverConnectionChanged);
- }
+ private static final String FLAG_KEY = "some-key";
+ private static final String FLAG_KEY_BOOLEAN = "some-key-boolean";
+ private static final String FLAG_KEY_INTEGER = "some-key-integer";
+ private static final String FLAG_KEY_DOUBLE = "some-key-double";
+ private static final String FLAG_KEY_STRING = "some-key-string";
+ private static final String FLAG_KEY_OBJECT = "some-key-object";
+ private static final String BOOL_VARIANT = "on";
+ private static final String DOUBLE_VARIANT = "half";
+ private static final String INT_VARIANT = "one-hundred";
+ private static final String STRING_VARIANT = "greeting";
+ private static final String OBJECT_VARIANT = "obj";
+ private static final Reason DEFAULT = Reason.DEFAULT;
+ private static final Integer INT_VALUE = 100;
+ private static final Double DOUBLE_VALUE = .5d;
+ private static final String INNER_STRUCT_KEY = "inner_key";
+ private static final String INNER_STRUCT_VALUE = "inner_value";
+ private static final com.google.protobuf.Struct PROTOBUF_STRUCTURE_VALUE = Struct.newBuilder()
+ .putFields(INNER_STRUCT_KEY,
+ com.google.protobuf.Value.newBuilder().setStringValue(INNER_STRUCT_VALUE)
+ .build())
+ .build();
+ private static final String STRING_VALUE = "hi!";
+
+ private static OpenFeatureAPI api;
+
+ @BeforeAll
+ public static void init() {
+ api = OpenFeatureAPI.getInstance();
+ }
+
+ @AfterAll
+ public static void cleanUp() {
+ api.shutdown();
+ }
+
+ @Test
+ void resolvers_call_grpc_service_and_return_details() {
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
+ .setValue(STRING_VALUE)
+ .setVariant(STRING_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
+ .setValue(INT_VALUE)
+ .setVariant(INT_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
+ .setValue(DOUBLE_VALUE)
+ .setVariant(DOUBLE_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
+ .setValue(PROTOBUF_STRUCTURE_VALUE)
+ .setVariant(OBJECT_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock
+ .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey()))))
+ .thenReturn(booleanResponse);
+ when(serviceBlockingStubMock
+ .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey()))))
+ .thenReturn(floatResponse);
+ when(serviceBlockingStubMock
+ .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey()))))
+ .thenReturn(intResponse);
+ when(serviceBlockingStubMock
+ .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey()))))
+ .thenReturn(stringResponse);
+ when(serviceBlockingStubMock
+ .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey()))))
+ .thenReturn(objectResponse);
+
+ GrpcConnector grpc = mock(GrpcConnector.class);
+ when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
+
+ OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
+
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN,
+ false);
+ assertTrue(booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(DEFAULT.toString(), booleanDetails.getReason());
+
+ FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING,
+ "wrong");
+ assertEquals(STRING_VALUE, stringDetails.getValue());
+ assertEquals(STRING_VARIANT, stringDetails.getVariant());
+ assertEquals(DEFAULT.toString(), stringDetails.getReason());
+
+ FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
+ assertEquals(INT_VALUE, intDetails.getValue());
+ assertEquals(INT_VARIANT, intDetails.getVariant());
+ assertEquals(DEFAULT.toString(), intDetails.getReason());
+
+ FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
+ assertEquals(DOUBLE_VALUE, floatDetails.getValue());
+ assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
+ assertEquals(DEFAULT.toString(), floatDetails.getReason());
+
+ FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT,
+ new Value());
+ assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
+ .asMap().get(INNER_STRUCT_KEY).asString());
+ assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
+ assertEquals(DEFAULT.toString(), objectDetails.getReason());
+ }
+
+ @Test
+ void zero_value() {
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setVariant(BOOL_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
+ .setVariant(STRING_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
+ .setVariant(INT_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
+ .setVariant(DOUBLE_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
+ .setVariant(OBJECT_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock
+ .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey()))))
+ .thenReturn(booleanResponse);
+ when(serviceBlockingStubMock
+ .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey()))))
+ .thenReturn(floatResponse);
+ when(serviceBlockingStubMock
+ .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey()))))
+ .thenReturn(intResponse);
+ when(serviceBlockingStubMock
+ .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey()))))
+ .thenReturn(stringResponse);
+ when(serviceBlockingStubMock
+ .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey()))))
+ .thenReturn(objectResponse);
+
+ GrpcConnector grpc = mock(GrpcConnector.class);
+ when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
+
+ OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
+
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN,
+ false);
+ assertEquals(false, booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(DEFAULT.toString(), booleanDetails.getReason());
+
+ FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING,
+ "wrong");
+ assertEquals("", stringDetails.getValue());
+ assertEquals(STRING_VARIANT, stringDetails.getVariant());
+ assertEquals(DEFAULT.toString(), stringDetails.getReason());
+
+ FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
+ assertEquals(0, intDetails.getValue());
+ assertEquals(INT_VARIANT, intDetails.getVariant());
+ assertEquals(DEFAULT.toString(), intDetails.getReason());
+
+ FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
+ assertEquals(0.0, floatDetails.getValue());
+ assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
+ assertEquals(DEFAULT.toString(), floatDetails.getReason());
+
+ FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT,
+ new Value());
+ assertEquals(new MutableStructure(), objectDetails.getValue().asObject());
+ assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
+ assertEquals(DEFAULT.toString(), objectDetails.getReason());
+ }
+
+ @Test
+ void test_metadata_from_grpc_response() {
+ // given
+ final Map metadataInput = new HashMap<>();
+
+ com.google.protobuf.Value scope = com.google.protobuf.Value.newBuilder().setStringValue("flagd-scope")
+ .build();
+ metadataInput.put("scope", scope);
+
+ com.google.protobuf.Value bool = com.google.protobuf.Value.newBuilder().setBoolValue(true).build();
+ metadataInput.put("boolean", bool);
+
+ com.google.protobuf.Value number = com.google.protobuf.Value.newBuilder().setNumberValue(1).build();
+ metadataInput.put("number", number);
+
+ final Struct metadataStruct = Struct.newBuilder().putAllFields(metadataInput).build();
+
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(DEFAULT.toString())
+ .setMetadata(metadataStruct)
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))).thenReturn(
+ serviceBlockingStubMock);
+ when(serviceBlockingStubMock
+ .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey()))))
+ .thenReturn(booleanResponse);
+
+ GrpcConnector grpc = mock(GrpcConnector.class);
+ when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
+ OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
+
+ // when
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN,
+ false);
+
+ // then
+ final ImmutableMetadata metadata = booleanDetails.getFlagMetadata();
- public void initialize() throws Exception {};
- }
+ assertEquals("flagd-scope", metadata.getString("scope"));
+ assertEquals(true, metadata.getBoolean("boolean"));
+ assertEquals(1, metadata.getDouble("number"));
+ }
+
+ @Test
+ void resolvers_cache_responses_if_static_and_event_stream_alive() {
+ do_resolvers_cache_responses(STATIC_REASON, true, true);
+ }
- grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys) -> {
- });
+ @Test
+ void resolvers_should_not_cache_responses_if_not_static() {
+ do_resolvers_cache_responses(DEFAULT.toString(), true, false);
}
- FlagdProvider provider = createProvider(grpc);
- OpenFeatureAPI.getInstance().setProviderAndWait(provider);
-
- HashMap flagsMap = new HashMap();
- HashMap structMap = new HashMap();
-
- flagsMap.put(FLAG_KEY_BOOLEAN, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
- flagsMap.put(FLAG_KEY_STRING, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
- flagsMap.put(FLAG_KEY_INTEGER, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
- flagsMap.put(FLAG_KEY_DOUBLE, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
- flagsMap.put(FLAG_KEY_OBJECT, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
-
- structMap.put("flags", com.google.protobuf.Value.newBuilder()
- .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build());
-
- // should cache results
- FlagEvaluationDetails booleanDetails;
- FlagEvaluationDetails stringDetails;
- FlagEvaluationDetails intDetails;
- FlagEvaluationDetails floatDetails;
- FlagEvaluationDetails objectDetails;
-
- // assert cache has been invalidated
- booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
- assertTrue(booleanDetails.getValue());
- assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
- assertEquals(STATIC_REASON, booleanDetails.getReason());
-
- stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
- assertEquals(STRING_VALUE, stringDetails.getValue());
- assertEquals(STRING_VARIANT, stringDetails.getVariant());
- assertEquals(STATIC_REASON, stringDetails.getReason());
-
- intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
- assertEquals(INT_VALUE, intDetails.getValue());
- assertEquals(INT_VARIANT, intDetails.getVariant());
- assertEquals(STATIC_REASON, intDetails.getReason());
-
- floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
- assertEquals(DOUBLE_VALUE, floatDetails.getValue());
- assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
- assertEquals(STATIC_REASON, floatDetails.getReason());
-
- objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
- assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
- .asMap().get(INNER_STRUCT_KEY).asString());
- assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
- assertEquals(STATIC_REASON, objectDetails.getReason());
- }
-
- private void do_resolvers_cache_responses(String reason, Boolean eventStreamAlive, Boolean shouldCache) {
- String expectedReason = CACHED_REASON;
- if (!shouldCache) {
- expectedReason = reason;
+ @Test
+ void resolvers_should_not_cache_responses_if_event_stream_not_alive() {
+ do_resolvers_cache_responses(STATIC_REASON, false, false);
}
- ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
- .setValue(true)
- .setVariant(BOOL_VARIANT)
- .setReason(reason)
- .build();
-
- ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
- .setValue(STRING_VALUE)
- .setVariant(STRING_VARIANT)
- .setReason(reason)
- .build();
-
- ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
- .setValue(INT_VALUE)
- .setVariant(INT_VARIANT)
- .setReason(reason)
- .build();
-
- ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
- .setValue(DOUBLE_VALUE)
- .setVariant(DOUBLE_VARIANT)
- .setReason(reason)
- .build();
-
- ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
- .setValue(PROTOBUF_STRUCTURE_VALUE)
- .setVariant(OBJECT_VARIANT)
- .setReason(reason)
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock
- .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))).thenReturn(booleanResponse);
- when(serviceBlockingStubMock
- .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))).thenReturn(floatResponse);
- when(serviceBlockingStubMock
- .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))).thenReturn(intResponse);
- when(serviceBlockingStubMock
- .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))).thenReturn(stringResponse);
- when(serviceBlockingStubMock
- .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))).thenReturn(objectResponse);
-
- GrpcConnector grpc = mock(GrpcConnector.class);
- when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
- FlagdProvider provider = createProvider(grpc, () -> eventStreamAlive);
- // provider.setState(eventStreamAlive); // caching only available when event
- // stream is alive
- OpenFeatureAPI.getInstance().setProviderAndWait(provider);
-
- FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
- booleanDetails = api.getClient()
- .getBooleanDetails(FLAG_KEY_BOOLEAN, false); // should retrieve from cache on second invocation
- assertTrue(booleanDetails.getValue());
- assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
- assertEquals(expectedReason, booleanDetails.getReason());
-
- FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
- stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
- assertEquals(STRING_VALUE, stringDetails.getValue());
- assertEquals(STRING_VARIANT, stringDetails.getVariant());
- assertEquals(expectedReason, stringDetails.getReason());
-
- FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
- intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
- assertEquals(INT_VALUE, intDetails.getValue());
- assertEquals(INT_VARIANT, intDetails.getVariant());
- assertEquals(expectedReason, intDetails.getReason());
-
- FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
- floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
- assertEquals(DOUBLE_VALUE, floatDetails.getValue());
- assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
- assertEquals(expectedReason, floatDetails.getReason());
-
- FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
- objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
- assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
- .asMap().get(INNER_STRUCT_KEY).asString());
- assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
- assertEquals(expectedReason, objectDetails.getReason());
- }
-
- @Test
- void disabled_cache() {
- ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
- .setValue(true)
- .setVariant(BOOL_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
- .setValue(STRING_VALUE)
- .setVariant(STRING_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
- .setValue(INT_VALUE)
- .setVariant(INT_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
- .setValue(DOUBLE_VALUE)
- .setVariant(DOUBLE_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
- .setValue(PROTOBUF_STRUCTURE_VALUE)
- .setVariant(OBJECT_VARIANT)
- .setReason(STATIC_REASON)
- .build();
-
- ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
- ServiceStub serviceStubMock = mock(ServiceStub.class);
- when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock);
- when(serviceStubMock.withDeadline(any(Deadline.class)))
- .thenReturn(serviceStubMock);
- when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock.withDeadline(any(Deadline.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
- .thenReturn(serviceBlockingStubMock);
- when(serviceBlockingStubMock
- .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))).thenReturn(booleanResponse);
- when(serviceBlockingStubMock
- .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))).thenReturn(floatResponse);
- when(serviceBlockingStubMock
- .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))).thenReturn(intResponse);
- when(serviceBlockingStubMock
- .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))).thenReturn(stringResponse);
- when(serviceBlockingStubMock
- .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))).thenReturn(objectResponse);
-
- // disabled cache
- final Cache cache = new Cache("disabled", 0);
-
- GrpcConnector grpc;
- try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) {
- mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class)))
+ @Test
+ void context_is_parsed_and_passed_to_grpc_service() {
+ final String BOOLEAN_ATTR_KEY = "bool-attr";
+ final String INT_ATTR_KEY = "int-attr";
+ final String STRING_ATTR_KEY = "string-attr";
+ final String STRUCT_ATTR_KEY = "struct-attr";
+ final String DOUBLE_ATTR_KEY = "double-attr";
+ final String LIST_ATTR_KEY = "list-attr";
+ final String STRUCT_ATTR_INNER_KEY = "struct-inner-key";
+
+ final Boolean BOOLEAN_ATTR_VALUE = true;
+ final int INT_ATTR_VALUE = 1;
+ final String STRING_ATTR_VALUE = "str";
+ final double DOUBLE_ATTR_VALUE = 0.5d;
+ final List LIST_ATTR_VALUE = new ArrayList() {
+ {
+ add(new Value(1));
+ }
+ };
+ final String STRUCT_ATTR_INNER_VALUE = "struct-inner-value";
+ final Structure STRUCT_ATTR_VALUE = new MutableStructure().add(STRUCT_ATTR_INNER_KEY,
+ STRUCT_ATTR_INNER_VALUE);
+ final String DEFAULT_STRING = "DEFAULT";
+
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(DEFAULT_STRING.toString())
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
.thenReturn(serviceBlockingStubMock);
- mockStaticService.when(() -> ServiceGrpc.newStub(any()))
+ when(serviceBlockingStubMock.resolveBoolean(argThat(
+ x -> {
+ final Struct struct = x.getContext();
+ final Map valueMap = struct.getFieldsMap();
+
+ return STRING_ATTR_VALUE.equals(valueMap.get(STRING_ATTR_KEY).getStringValue())
+ && INT_ATTR_VALUE == valueMap.get(INT_ATTR_KEY).getNumberValue()
+ && DOUBLE_ATTR_VALUE == valueMap.get(DOUBLE_ATTR_KEY)
+ .getNumberValue()
+ && valueMap.get(BOOLEAN_ATTR_KEY).getBoolValue()
+ && "MY_TARGETING_KEY".equals(
+ valueMap.get("targetingKey").getStringValue())
+ && LIST_ATTR_VALUE.get(0).asInteger() == valueMap
+ .get(LIST_ATTR_KEY).getListValue()
+ .getValuesList().get(0).getNumberValue()
+ && STRUCT_ATTR_INNER_VALUE.equals(
+ valueMap.get(STRUCT_ATTR_KEY).getStructValue()
+ .getFieldsMap()
+ .get(STRUCT_ATTR_INNER_KEY)
+ .getStringValue());
+ }))).thenReturn(booleanResponse);
+
+ GrpcConnector grpc = mock(GrpcConnector.class);
+ when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
+
+ OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
+
+ final MutableContext context = new MutableContext("MY_TARGETING_KEY");
+ context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE);
+ context.add(INT_ATTR_KEY, INT_ATTR_VALUE);
+ context.add(DOUBLE_ATTR_KEY, DOUBLE_ATTR_VALUE);
+ context.add(LIST_ATTR_KEY, LIST_ATTR_VALUE);
+ context.add(STRING_ATTR_KEY, STRING_ATTR_VALUE);
+ context.add(STRUCT_ATTR_KEY, STRUCT_ATTR_VALUE);
+
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false,
+ context);
+ assertTrue(booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(DEFAULT.toString(), booleanDetails.getReason());
+ }
+
+ // Validates null handling -
+ // https://github.com/open-feature/java-sdk-contrib/issues/258
+ @Test
+ void null_context_handling() {
+ // given
+ final String flagA = "flagA";
+ final boolean defaultVariant = false;
+ final boolean expectedVariant = true;
+
+ final MutableContext context = new MutableContext();
+ context.add("key", (String) null);
+
+ final ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+
+ // when
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock.resolveBoolean(any()))
+ .thenReturn(ResolveBooleanResponse.newBuilder().setValue(expectedVariant).build());
+
+ GrpcConnector grpc = mock(GrpcConnector.class);
+ when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
+
+ OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
+
+ // then
+ final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context);
+
+ assertNotEquals(evaluation, defaultVariant);
+ assertEquals(evaluation, expectedVariant);
+ }
+
+ @Test
+ void reason_mapped_correctly_if_unknown() {
+ ResolveBooleanResponse badReasonResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason("UNKNOWN") // set an invalid reason string
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock.resolveBoolean(any(ResolveBooleanRequest.class)))
+ .thenReturn(badReasonResponse);
+
+ GrpcConnector grpc = mock(GrpcConnector.class);
+ when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
+
+ OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc));
+
+ FlagEvaluationDetails booleanDetails = api.getClient()
+ .getBooleanDetails(FLAG_KEY, false, new MutableContext());
+ assertEquals(Reason.UNKNOWN.toString(), booleanDetails.getReason()); // reason should be converted to
+ // UNKNOWN
+ }
+
+ @Test
+ void invalidate_cache() {
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
+ .setValue(STRING_VALUE)
+ .setVariant(STRING_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
+ .setValue(INT_VALUE)
+ .setVariant(INT_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
+ .setValue(DOUBLE_VALUE)
+ .setVariant(DOUBLE_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
+ .setValue(PROTOBUF_STRUCTURE_VALUE)
+ .setVariant(OBJECT_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ ServiceStub serviceStubMock = mock(ServiceStub.class);
+ when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock);
+ doNothing().when(serviceStubMock).eventStream(any(), any());
+ when(serviceStubMock.withDeadline(any(Deadline.class)))
.thenReturn(serviceStubMock);
+ when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock
+ .withDeadline(any(Deadline.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock
+ .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey()))))
+ .thenReturn(booleanResponse);
+ when(serviceBlockingStubMock
+ .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey()))))
+ .thenReturn(floatResponse);
+ when(serviceBlockingStubMock
+ .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey()))))
+ .thenReturn(intResponse);
+ when(serviceBlockingStubMock
+ .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey()))))
+ .thenReturn(stringResponse);
+ when(serviceBlockingStubMock
+ .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey()))))
+ .thenReturn(objectResponse);
+
+ GrpcConnector grpc;
+ try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) {
+ mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class)))
+ .thenReturn(serviceBlockingStubMock);
+ mockStaticService.when(() -> ServiceGrpc.newStub(any()))
+ .thenReturn(serviceStubMock);
+
+ final Cache cache = new Cache("lru", 5);
+
+ class NoopInitGrpcConnector extends GrpcConnector {
+ public NoopInitGrpcConnector(FlagdOptions options, Cache cache,
+ Supplier connectedSupplier,
+ Consumer onConnectionEvent) {
+ super(options, cache, connectedSupplier, onConnectionEvent);
+ }
+
+ public void initialize() throws Exception {
+ };
+ }
+
+ grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true,
+ (connectionEvent) -> {
+ });
+ }
+
+ FlagdProvider provider = createProvider(grpc);
+ OpenFeatureAPI.getInstance().setProviderAndWait(provider);
+
+ HashMap flagsMap = new HashMap();
+ HashMap structMap = new HashMap();
+
+ flagsMap.put(FLAG_KEY_BOOLEAN, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
+ flagsMap.put(FLAG_KEY_STRING, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
+ flagsMap.put(FLAG_KEY_INTEGER, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
+ flagsMap.put(FLAG_KEY_DOUBLE, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
+ flagsMap.put(FLAG_KEY_OBJECT, com.google.protobuf.Value.newBuilder().setStringValue("foo").build());
+
+ structMap.put("flags", com.google.protobuf.Value.newBuilder()
+ .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build());
+
+ // should cache results
+ FlagEvaluationDetails booleanDetails;
+ FlagEvaluationDetails stringDetails;
+ FlagEvaluationDetails intDetails;
+ FlagEvaluationDetails floatDetails;
+ FlagEvaluationDetails objectDetails;
+
+ // assert cache has been invalidated
+ booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
+ assertTrue(booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(STATIC_REASON, booleanDetails.getReason());
+
+ stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
+ assertEquals(STRING_VALUE, stringDetails.getValue());
+ assertEquals(STRING_VARIANT, stringDetails.getVariant());
+ assertEquals(STATIC_REASON, stringDetails.getReason());
+
+ intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
+ assertEquals(INT_VALUE, intDetails.getValue());
+ assertEquals(INT_VARIANT, intDetails.getVariant());
+ assertEquals(STATIC_REASON, intDetails.getReason());
+
+ floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
+ assertEquals(DOUBLE_VALUE, floatDetails.getValue());
+ assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
+ assertEquals(STATIC_REASON, floatDetails.getReason());
+
+ objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
+ assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
+ .asMap().get(INNER_STRUCT_KEY).asString());
+ assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
+ assertEquals(STATIC_REASON, objectDetails.getReason());
+ }
+
+ private void do_resolvers_cache_responses(String reason, Boolean eventStreamAlive, Boolean shouldCache) {
+ String expectedReason = CACHED_REASON;
+ if (!shouldCache) {
+ expectedReason = reason;
+ }
- class NoopInitGrpcConnector extends GrpcConnector {
- public NoopInitGrpcConnector(FlagdOptions options, Cache cache,
- Supplier connectedSupplier,
- BiConsumer> onResolverConnectionChanged) {
- super(options, cache, connectedSupplier, onResolverConnectionChanged);
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(reason)
+ .build();
+
+ ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
+ .setValue(STRING_VALUE)
+ .setVariant(STRING_VARIANT)
+ .setReason(reason)
+ .build();
+
+ ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
+ .setValue(INT_VALUE)
+ .setVariant(INT_VARIANT)
+ .setReason(reason)
+ .build();
+
+ ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
+ .setValue(DOUBLE_VALUE)
+ .setVariant(DOUBLE_VARIANT)
+ .setReason(reason)
+ .build();
+
+ ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
+ .setValue(PROTOBUF_STRUCTURE_VALUE)
+ .setVariant(OBJECT_VARIANT)
+ .setReason(reason)
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock
+ .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey()))))
+ .thenReturn(booleanResponse);
+ when(serviceBlockingStubMock
+ .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey()))))
+ .thenReturn(floatResponse);
+ when(serviceBlockingStubMock
+ .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey()))))
+ .thenReturn(intResponse);
+ when(serviceBlockingStubMock
+ .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey()))))
+ .thenReturn(stringResponse);
+ when(serviceBlockingStubMock
+ .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey()))))
+ .thenReturn(objectResponse);
+
+ GrpcConnector grpc = mock(GrpcConnector.class);
+ when(grpc.getResolver()).thenReturn(serviceBlockingStubMock);
+ FlagdProvider provider = createProvider(grpc, () -> eventStreamAlive);
+ // provider.setState(eventStreamAlive); // caching only available when event
+ // stream is alive
+ OpenFeatureAPI.getInstance().setProviderAndWait(provider);
+
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN,
+ false);
+ booleanDetails = api.getClient()
+ .getBooleanDetails(FLAG_KEY_BOOLEAN, false); // should retrieve from cache on second
+ // invocation
+ assertTrue(booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(expectedReason, booleanDetails.getReason());
+
+ FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING,
+ "wrong");
+ stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
+ assertEquals(STRING_VALUE, stringDetails.getValue());
+ assertEquals(STRING_VARIANT, stringDetails.getVariant());
+ assertEquals(expectedReason, stringDetails.getReason());
+
+ FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
+ intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
+ assertEquals(INT_VALUE, intDetails.getValue());
+ assertEquals(INT_VARIANT, intDetails.getVariant());
+ assertEquals(expectedReason, intDetails.getReason());
+
+ FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
+ floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
+ assertEquals(DOUBLE_VALUE, floatDetails.getValue());
+ assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
+ assertEquals(expectedReason, floatDetails.getReason());
+
+ FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT,
+ new Value());
+ objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
+ assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
+ .asMap().get(INNER_STRUCT_KEY).asString());
+ assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
+ assertEquals(expectedReason, objectDetails.getReason());
+ }
+
+ @Test
+ void disabled_cache() {
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
+ .setValue(STRING_VALUE)
+ .setVariant(STRING_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
+ .setValue(INT_VALUE)
+ .setVariant(INT_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
+ .setValue(DOUBLE_VALUE)
+ .setVariant(DOUBLE_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
+ .setValue(PROTOBUF_STRUCTURE_VALUE)
+ .setVariant(OBJECT_VARIANT)
+ .setReason(STATIC_REASON)
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ ServiceStub serviceStubMock = mock(ServiceStub.class);
+ when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock);
+ when(serviceStubMock.withDeadline(any(Deadline.class)))
+ .thenReturn(serviceStubMock);
+ when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock.withDeadline(any(Deadline.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class)))
+ .thenReturn(serviceBlockingStubMock);
+ when(serviceBlockingStubMock
+ .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey()))))
+ .thenReturn(booleanResponse);
+ when(serviceBlockingStubMock
+ .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey()))))
+ .thenReturn(floatResponse);
+ when(serviceBlockingStubMock
+ .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey()))))
+ .thenReturn(intResponse);
+ when(serviceBlockingStubMock
+ .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey()))))
+ .thenReturn(stringResponse);
+ when(serviceBlockingStubMock
+ .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey()))))
+ .thenReturn(objectResponse);
+
+ // disabled cache
+ final Cache cache = new Cache("disabled", 0);
+
+ GrpcConnector grpc;
+ try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) {
+ mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class)))
+ .thenReturn(serviceBlockingStubMock);
+ mockStaticService.when(() -> ServiceGrpc.newStub(any()))
+ .thenReturn(serviceStubMock);
+
+ class NoopInitGrpcConnector extends GrpcConnector {
+ public NoopInitGrpcConnector(FlagdOptions options, Cache cache,
+ Supplier connectedSupplier,
+ Consumer onConnectionEvent) {
+ super(options, cache, connectedSupplier, onConnectionEvent);
+ }
+
+ public void initialize() throws Exception {
+ };
}
- public void initialize() throws Exception {
- };
+ grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true,
+ (connectionEvent) -> {
+ });
}
- grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys) -> {
- });
+ FlagdProvider provider = createProvider(grpc, cache, () -> true);
+
+ try {
+ provider.initialize(null);
+ } catch (Exception e) {
+ // ignore exception if any
+ }
+
+ OpenFeatureAPI.getInstance().setProviderAndWait(provider);
+
+ HashMap flagsMap = new HashMap<>();
+ HashMap structMap = new HashMap<>();
+
+ flagsMap.put("foo", com.google.protobuf.Value.newBuilder().setStringValue("foo")
+ .build()); // assert that a configuration_change event works
+
+ structMap.put("flags", com.google.protobuf.Value.newBuilder()
+ .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build());
+
+ // should not cache results
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN,
+ false);
+ FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING,
+ "wrong");
+ FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
+ FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
+ FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT,
+ new Value());
+
+ // assert values are not cached
+ booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
+ assertTrue(booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(STATIC_REASON, booleanDetails.getReason());
+
+ stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
+ assertEquals(STRING_VALUE, stringDetails.getValue());
+ assertEquals(STRING_VARIANT, stringDetails.getVariant());
+ assertEquals(STATIC_REASON, stringDetails.getReason());
+
+ intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
+ assertEquals(INT_VALUE, intDetails.getValue());
+ assertEquals(INT_VARIANT, intDetails.getVariant());
+ assertEquals(STATIC_REASON, intDetails.getReason());
+
+ floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
+ assertEquals(DOUBLE_VALUE, floatDetails.getValue());
+ assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
+ assertEquals(STATIC_REASON, floatDetails.getReason());
+
+ objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
+ assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
+ .asMap().get(INNER_STRUCT_KEY).asString());
+ assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
+ assertEquals(STATIC_REASON, objectDetails.getReason());
}
- FlagdProvider provider = createProvider(grpc, cache, () -> true);
+ @Test
+ void contextMerging() throws Exception {
+ // given
+ final FlagdProvider provider = new FlagdProvider();
+
+ final Resolver resolverMock = mock(Resolver.class);
- try {
- provider.initialize(null);
- } catch (Exception e) {
- // ignore exception if any
+ Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver");
+ flagResolver.setAccessible(true);
+ flagResolver.set(provider, resolverMock);
+
+ final HashMap globalCtxMap = new HashMap<>();
+ globalCtxMap.put("id", new Value("GlobalID"));
+ globalCtxMap.put("env", new Value("A"));
+
+ final HashMap localCtxMap = new HashMap<>();
+ localCtxMap.put("id", new Value("localID"));
+ localCtxMap.put("client", new Value("999"));
+
+ final HashMap expectedCtx = new HashMap<>();
+ expectedCtx.put("id", new Value("localID"));
+ expectedCtx.put("env", new Value("A"));
+ localCtxMap.put("client", new Value("999"));
+
+ // when
+ provider.initialize(new ImmutableContext(globalCtxMap));
+ provider.getBooleanEvaluation("ket", false, new ImmutableContext(localCtxMap));
+
+ // then
+ verify(resolverMock).booleanEvaluation(any(), any(), argThat(
+ ctx -> ctx.asMap().entrySet().containsAll(expectedCtx.entrySet())));
}
- OpenFeatureAPI.getInstance().setProviderAndWait(provider);
+ @Test
+ void initializationAndShutdown() throws Exception {
+ // given
+ final FlagdProvider provider = new FlagdProvider();
+ final EvaluationContext ctx = new ImmutableContext();
+
+ final Resolver resolverMock = mock(Resolver.class);
+
+ Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver");
+ flagResolver.setAccessible(true);
+ flagResolver.set(provider, resolverMock);
+
+ // when
+
+ // validate multiple initialization
+ provider.initialize(ctx);
+ provider.initialize(ctx);
+
+ // validate multiple shutdowns
+ provider.shutdown();
+ provider.shutdown();
- HashMap flagsMap = new HashMap<>();
- HashMap structMap = new HashMap<>();
-
- flagsMap.put("foo", com.google.protobuf.Value.newBuilder().setStringValue("foo")
- .build()); // assert that a configuration_change event works
+ // then
+ verify(resolverMock, times(1)).init();
+ verify(resolverMock, times(1)).shutdown();
+ }
- structMap.put("flags", com.google.protobuf.Value.newBuilder()
- .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build());
-
- // should not cache results
- FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
- FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
- FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
- FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
- FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
+ @Test
+ void updatesSyncMetadataWithCallback() throws Exception {
+
+ final EvaluationContext ctx = new ImmutableContext();
+ String key = "key1";
+ String val = "val1";
+ Map metadata = new HashMap<>();
+ metadata.put(key, val);
+
+ // mock a resolver
+ try (MockedConstruction mockResolver = mockConstruction(GrpcResolver.class,
+ (mock, context) -> {
+ Consumer onConnectionEvent;
+
+ // get a reference to the onConnectionEvent callback
+ onConnectionEvent = (Consumer) context
+ .arguments().get(3);
+
+ // when our mock resolver initializes, it runs the passed onConnectionEvent
+ // callback
+ doAnswer(invocation -> {
+ onConnectionEvent.accept(
+ new ConnectionEvent(true, metadata));
+ return null;
+ }).when(mock).init();
+ })) {
+
+ FlagdProvider provider = new FlagdProvider();
+ provider.initialize(ctx);
+
+ // the onConnectionEvent should have updated the sync metadata
+ assertEquals(val, provider.getSyncMetadata().get(key));
+ }
+ }
- // assert values are not cached
- booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false);
- assertTrue(booleanDetails.getValue());
- assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
- assertEquals(STATIC_REASON, booleanDetails.getReason());
-
- stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong");
- assertEquals(STRING_VALUE, stringDetails.getValue());
- assertEquals(STRING_VARIANT, stringDetails.getVariant());
- assertEquals(STATIC_REASON, stringDetails.getReason());
+ // test helper
+
+ // create provider with given grpc connector
+ private FlagdProvider createProvider(GrpcConnector grpc) {
+ return createProvider(grpc, () -> true);
+ }
- intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0);
- assertEquals(INT_VALUE, intDetails.getValue());
- assertEquals(INT_VARIANT, intDetails.getVariant());
- assertEquals(STATIC_REASON, intDetails.getReason());
+ // create provider with given grpc provider and state supplier
+ private FlagdProvider createProvider(GrpcConnector grpc, Supplier getConnected) {
+ final Cache cache = new Cache("lru", 5);
- floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1);
- assertEquals(DOUBLE_VALUE, floatDetails.getValue());
- assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
- assertEquals(STATIC_REASON, floatDetails.getReason());
-
- objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value());
- assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure()
- .asMap().get(INNER_STRUCT_KEY).asString());
- assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
- assertEquals(STATIC_REASON, objectDetails.getReason());
- }
-
- @Test
- void contextMerging() throws Exception {
- // given
- final FlagdProvider provider = new FlagdProvider();
-
- final Resolver resolverMock = mock(Resolver.class);
-
- Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver");
- flagResolver.setAccessible(true);
- flagResolver.set(provider, resolverMock);
-
- final HashMap globalCtxMap = new HashMap<>();
- globalCtxMap.put("id", new Value("GlobalID"));
- globalCtxMap.put("env", new Value("A"));
-
- final HashMap localCtxMap = new HashMap<>();
- localCtxMap.put("id", new Value("localID"));
- localCtxMap.put("client", new Value("999"));
-
- final HashMap expectedCtx = new HashMap<>();
- expectedCtx.put("id", new Value("localID"));
- expectedCtx.put("env", new Value("A"));
- localCtxMap.put("client", new Value("999"));
-
- // when
- provider.initialize(new ImmutableContext(globalCtxMap));
- provider.getBooleanEvaluation("ket", false, new ImmutableContext(localCtxMap));
-
- // then
- verify(resolverMock).booleanEvaluation(any(), any(), argThat(
- ctx -> ctx.asMap().entrySet().containsAll(expectedCtx.entrySet())));
- }
-
- @Test
- void initializationAndShutdown() throws Exception {
- // given
- final FlagdProvider provider = new FlagdProvider();
- final EvaluationContext ctx = new ImmutableContext();
-
- final Resolver resolverMock = mock(Resolver.class);
-
- Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver");
- flagResolver.setAccessible(true);
- flagResolver.set(provider, resolverMock);
-
- // when
-
- // validate multiple initialization
- provider.initialize(ctx);
- provider.initialize(ctx);
-
- // validate multiple shutdowns
- provider.shutdown();
- provider.shutdown();
-
- // then
- verify(resolverMock, times(1)).init();
- verify(resolverMock, times(1)).shutdown();
- }
-
- // test helper
-
- // create provider with given grpc connector
- private FlagdProvider createProvider(GrpcConnector grpc) {
- return createProvider(grpc, () -> true);
- }
-
- // create provider with given grpc provider and state supplier
- private FlagdProvider createProvider(GrpcConnector grpc, Supplier getConnected) {
- final Cache cache = new Cache("lru", 5);
-
- return createProvider(grpc, cache, getConnected);
- }
-
- // create provider with given grpc provider, cache and state supplier
- private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier getConnected) {
- final FlagdOptions flagdOptions = FlagdOptions.builder().build();
- final GrpcResolver grpcResolver = new GrpcResolver(flagdOptions, cache, getConnected,
- (providerState, changedFlagKeys) -> {
- });
-
- final FlagdProvider provider = new FlagdProvider();
-
- try {
- Field connector = GrpcResolver.class.getDeclaredField("connector");
- connector.setAccessible(true);
- connector.set(grpcResolver, grpc);
-
- Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver");
- flagResolver.setAccessible(true);
- flagResolver.set(provider, grpcResolver);
- } catch (NoSuchFieldException | IllegalAccessException e) {
- throw new RuntimeException(e);
+ return createProvider(grpc, cache, getConnected);
}
- 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);
+ // create provider with given grpc provider, cache and state supplier
+ private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier getConnected) {
+ final FlagdOptions flagdOptions = FlagdOptions.builder().build();
+ final GrpcResolver grpcResolver = new GrpcResolver(flagdOptions, cache, getConnected,
+ (connectionEvent) -> {
+ });
+
+ final FlagdProvider provider = new FlagdProvider();
+
+ try {
+ Field connector = GrpcResolver.class.getDeclaredField("connector");
+ connector.setAccessible(true);
+ connector.set(grpcResolver, grpc);
+
+ Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver");
+ flagResolver.setAccessible(true);
+ flagResolver.set(provider, grpcResolver);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+
+ return provider;
}
- 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/e2e/process/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/FlagdInProcessSetup.java
index a101a631d..6217e4830 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/FlagdInProcessSetup.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/FlagdInProcessSetup.java
@@ -26,6 +26,7 @@ public static void setup() throws InterruptedException {
flagdContainer.start();
FlagdInProcessSetup.provider = new FlagdProvider(FlagdOptions.builder()
.resolverType(Config.Resolver.IN_PROCESS)
+ // set a generous deadline, to prevent timeouts in actions
.deadline(3000)
.port(flagdContainer.getFirstMappedPort())
.build());
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
index 3ad3ac5f4..1140c04c7 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
@@ -23,8 +23,9 @@ public static void setup() throws InterruptedException {
flagdContainer.start();
FeatureProvider workingProvider = new FlagdProvider(FlagdOptions.builder()
.resolverType(Config.Resolver.IN_PROCESS)
- .deadline(3000)
.port(flagdContainer.getFirstMappedPort())
+ // set a generous deadline, to prevent timeouts in actions
+ .deadline(3000)
.build());
StepDefinitions.setUnstableProvider(workingProvider);
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java
index 945baa3cc..e323cfc7c 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java
@@ -37,7 +37,6 @@ public static void setup() throws InterruptedException {
FeatureProvider unavailableProvider = new FlagdProvider(FlagdOptions.builder()
.resolverType(Config.Resolver.RPC)
.port(8015) // this port isn't serving anything, error expected
- // set a generous deadline, to prevent timeouts in actions
.deadline(100)
.cacheType(CacheType.DISABLED.getValue())
.build());
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java
index d07c03259..525ee23f1 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java
@@ -93,7 +93,7 @@ public void cacheBustingForKnownKeys() {
Value flagsValue = mock(Value.class);
Struct flagsStruct = mock(Struct.class);
HashMap fields = new HashMap<>();
- fields.put(EventStreamObserver.FLAGS_KEY, flagsValue);
+ fields.put(Constants.FLAGS_KEY, flagsValue);
HashMap flags = new HashMap<>();
flags.put(key1, null);
flags.put(key2, null);
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java
index 3325341ed..59e7b6897 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java
@@ -18,7 +18,7 @@
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
@@ -27,9 +27,12 @@
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
+import org.mockito.invocation.InvocationOnMock;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
+import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse;
import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc;
import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceBlockingStub;
import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceStub;
@@ -43,7 +46,7 @@
public class GrpcConnectorTest {
@ParameterizedTest
- @ValueSource(ints = {1, 2, 3})
+ @ValueSource(ints = { 1, 2, 3 })
void validate_retry_calls(int retries) throws NoSuchFieldException, IllegalAccessException {
final int backoffMs = 100;
@@ -58,8 +61,9 @@ void validate_retry_calls(int retries) throws NoSuchFieldException, IllegalAcces
final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class);
doAnswer(invocation -> null).when(mockStub).eventStream(any(), any());
- final GrpcConnector connector = new GrpcConnector(options, cache, () -> true, (state,changedFlagKeys) -> {
- });
+ final GrpcConnector connector = new GrpcConnector(options, cache, () -> true,
+ (connectionEvent) -> {
+ });
Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub");
serviceStubField.setAccessible(true);
@@ -90,29 +94,69 @@ void validate_retry_calls(int retries) throws NoSuchFieldException, IllegalAcces
@Test
void initialization_succeed_with_connected_status() throws NoSuchFieldException, IllegalAccessException {
final Cache cache = new Cache("disabled", 0);
-
final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class);
- doAnswer(invocation -> null).when(mockStub).eventStream(any(), any());
+ Consumer onConnectionEvent = mock(Consumer.class);
+ doAnswer((InvocationOnMock invocation) -> {
+ EventStreamObserver eventStreamObserver = (EventStreamObserver) invocation.getArgument(1);
+ eventStreamObserver
+ .onNext(EventStreamResponse.newBuilder().setType(Constants.PROVIDER_READY).build());
+ return null;
+ }).when(mockStub).eventStream(any(), any());
- // pass true in connected lambda
- final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys) -> {
- });
+ try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) {
+ mockStaticService.when(() -> ServiceGrpc.newStub(any()))
+ .thenReturn(mockStub);
+
+ // pass true in connected lambda
+ final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, () -> {
+ try {
+ Thread.sleep(100);
+ return true;
+ } catch (Exception e) {
+ }
+ return false;
- assertDoesNotThrow(connector::initialize);
+ },
+ onConnectionEvent);
+
+ assertDoesNotThrow(connector::initialize);
+ // assert that onConnectionEvent is connected
+ verify(onConnectionEvent).accept(argThat(arg -> arg.isConnected()));
+ }
}
@Test
void initialization_fail_with_timeout() throws Exception {
final Cache cache = new Cache("disabled", 0);
-
final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class);
- doAnswer(invocation -> null).when(mockStub).eventStream(any(), any());
+ Consumer onConnectionEvent = mock(Consumer.class);
+ doAnswer((InvocationOnMock invocation) -> {
+ EventStreamObserver eventStreamObserver = (EventStreamObserver) invocation.getArgument(1);
+ eventStreamObserver
+ .onError(new Exception("fake"));
+ return null;
+ }).when(mockStub).eventStream(any(), any());
- // pass false in connected lambda
- final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, () -> false, (state, changedFlagKeys) -> {
- });
+ try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) {
+ mockStaticService.when(() -> ServiceGrpc.newStub(any()))
+ .thenReturn(mockStub);
- assertThrows(RuntimeException.class, connector::initialize);
+ // pass true in connected lambda
+ final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, () -> {
+ try {
+ Thread.sleep(100);
+ return true;
+ } catch (Exception e) {
+ }
+ return false;
+
+ },
+ onConnectionEvent);
+
+ assertDoesNotThrow(connector::initialize);
+ // assert that onConnectionEvent is connected
+ verify(onConnectionEvent).accept(argThat(arg -> !arg.isConnected()));
+ }
}
@Test
@@ -170,17 +214,16 @@ void no_args_host_and_port_env_set_should_build_tcp_socket() throws Exception {
new GrpcConnector(FlagdOptions.builder().build(), null, null, null);
// verify host/port matches & called times(= 1 as we rely on reusable channel)
- mockStaticChannelBuilder.verify(() -> NettyChannelBuilder.
- forAddress(host, port), times(1));
+ mockStaticChannelBuilder.verify(() -> NettyChannelBuilder.forAddress(host, port), times(1));
}
}
});
}
-
/**
- * OS Specific test - This test is valid only on Linux system as it rely on epoll availability
- * */
+ * OS Specific test - This test is valid only on Linux system as it rely on
+ * epoll availability
+ */
@Test
@EnabledOnOs(OS.LINUX)
void path_arg_should_build_domain_socket_with_correct_path() {
@@ -218,8 +261,9 @@ void path_arg_should_build_domain_socket_with_correct_path() {
}
/**
- * OS Specific test - This test is valid only on Linux system as it rely on epoll availability
- * */
+ * OS Specific test - This test is valid only on Linux system as it rely on
+ * epoll availability
+ */
@Test
@EnabledOnOs(OS.LINUX)
void no_args_socket_env_should_build_domain_socket_with_correct_path() throws Exception {
@@ -249,7 +293,7 @@ void no_args_socket_env_should_build_domain_socket_with_correct_path() throws Ex
new GrpcConnector(FlagdOptions.builder().build(), null, null, null);
- //verify path matches & called times(= 1 as we rely on reusable channel)
+ // verify path matches & called times(= 1 as we rely on reusable channel)
mockStaticChannelBuilder.verify(() -> NettyChannelBuilder
.forAddress(argThat((DomainSocketAddress d) -> {
return d.path() == path;
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java
index f3c2a5773..b66c65d3c 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java
@@ -2,6 +2,7 @@
import dev.openfeature.contrib.providers.flagd.Config;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.MockConnector;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState;
@@ -19,6 +20,7 @@
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.ParseError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
+
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -32,6 +34,7 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.BOOLEAN_FLAG;
import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.DISABLED_FLAG;
@@ -73,10 +76,16 @@ public void eventHandling() throws Throwable {
// given
// note - queues with adequate capacity
final BlockingQueue sender = new LinkedBlockingQueue<>(5);
- final BlockingQueue receiver = new LinkedBlockingQueue<>(5);
+ final BlockingQueue receiver = new LinkedBlockingQueue<>(5);
+ final String key = "key1";
+ final String val = "val1";
+ final Map syncMetadata = new HashMap<>();
+ syncMetadata.put(key, val);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender),
- (connectedState, changedFlagKeys) -> receiver.offer(connectedState));
+ (connectionEvent) -> receiver.offer(new StorageStateChange(
+ connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR,
+ connectionEvent.getFlagsChanged(), connectionEvent.getSyncMetadata())));
// when - init and emit events
Thread initThread = new Thread(() -> {
@@ -86,7 +95,7 @@ public void eventHandling() throws Throwable {
}
});
initThread.start();
- if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.EMPTY_LIST), 100,
+ if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.emptyList(), syncMetadata), 100,
TimeUnit.MILLISECONDS)) {
Assertions.fail("failed to send the event");
}
@@ -96,11 +105,13 @@ public void eventHandling() throws Throwable {
// then - receive events in order
assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
- Assertions.assertTrue(receiver.take());
+ StorageStateChange storageState = receiver.take();
+ assertEquals(StorageState.OK, storageState.getStorageState());
+ assertEquals(val, storageState.getSyncMetadata().get(key));
});
assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
- Assertions.assertFalse(receiver.take());
+ assertEquals(StorageState.ERROR, receiver.take().getStorageState());
});
}
@@ -111,7 +122,7 @@ public void simpleBooleanResolving() throws Exception {
flagMap.put("booleanFlag", BOOLEAN_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -132,7 +143,7 @@ public void simpleDoubleResolving() throws Exception {
flagMap.put("doubleFlag", DOUBLE_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -152,7 +163,7 @@ public void fetchIntegerAsDouble() throws Exception {
flagMap.put("doubleFlag", DOUBLE_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -172,7 +183,7 @@ public void fetchDoubleAsInt() throws Exception {
flagMap.put("integerFlag", INT_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -192,7 +203,7 @@ public void simpleIntResolving() throws Exception {
flagMap.put("integerFlag", INT_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -212,7 +223,7 @@ public void simpleObjectResolving() throws Exception {
flagMap.put("objectFlag", OBJECT_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
Map typeDefault = new HashMap<>();
@@ -239,7 +250,7 @@ public void missingFlag() throws Exception {
final Map flagMap = new HashMap<>();
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when/then
@@ -255,7 +266,7 @@ public void disabledFlag() throws Exception {
flagMap.put("disabledFlag", DISABLED_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when/then
@@ -271,7 +282,7 @@ public void variantMismatchFlag() throws Exception {
flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when/then
@@ -287,7 +298,7 @@ public void typeMismatchEvaluation() throws Exception {
flagMap.put("stringFlag", BOOLEAN_FLAG);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when/then
@@ -303,7 +314,7 @@ public void booleanShorthandEvaluation() throws Exception {
flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false,
@@ -322,7 +333,7 @@ public void targetingMatchedEvaluationFlag() throws Exception {
flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -343,7 +354,7 @@ public void targetingUnmatchedEvaluationFlag() throws Exception {
flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -364,7 +375,7 @@ public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalA
flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when
@@ -384,7 +395,7 @@ public void targetingErrorEvaluationFlag() throws Exception {
flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET);
InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap),
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
// when/then
@@ -419,17 +430,17 @@ private InProcessResolver getInProcessResolverWth(final FlagdOptions options, fi
throws NoSuchFieldException, IllegalAccessException {
final InProcessResolver resolver = new InProcessResolver(options, () -> true,
- (providerState, changedFlagKeys) -> {
+ (connectionEvent) -> {
});
return injectFlagStore(resolver, storage);
}
private InProcessResolver getInProcessResolverWth(final MockStorage storage,
- final BiConsumer> stateConsumer)
+ final Consumer onConnectionEvent)
throws NoSuchFieldException, IllegalAccessException {
final InProcessResolver resolver = new InProcessResolver(
- FlagdOptions.builder().deadline(1000).build(), () -> true, stateConsumer);
+ FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent);
return injectFlagStore(resolver, storage);
}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java
index 3e69d48c5..571ffdb5e 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java
@@ -1,11 +1,11 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;
-import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
-import dev.openfeature.contrib.providers.flagd.resolver.process.model.FlagParser;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType;
-import org.junit.Assert;
-import org.junit.jupiter.api.Test;
+import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG;
+import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG;
+import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE;
+import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.getFlagsFromResource;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import java.time.Duration;
import java.util.HashSet;
@@ -14,20 +14,22 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
-import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG;
-import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG;
-import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE;
-import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.getFlagsFromResource;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+
+import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
+import dev.openfeature.contrib.providers.flagd.resolver.process.model.FlagParser;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType;
+import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
class FlagStoreTest {
@Test
public void connectorHandling() throws Exception {
- final int maxDelay = 500;
+ final int maxDelay = 1000;
- final BlockingQueue payload = new LinkedBlockingQueue<>();
+ final BlockingQueue payload = new LinkedBlockingQueue<>();
FlagStore store = new FlagStore(new MockConnector(payload), true);
store.init();
@@ -35,7 +37,7 @@ public void connectorHandling() throws Exception {
// OK for simple flag
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
- payload.offer(new StreamPayload(StreamPayloadType.DATA, getFlagsFromResource(VALID_SIMPLE)));
+ payload.offer(new QueuePayload(QueuePayloadType.DATA, getFlagsFromResource(VALID_SIMPLE), GetMetadataResponse.getDefaultInstance()));
});
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
@@ -44,7 +46,7 @@ public void connectorHandling() throws Exception {
// STALE for invalid flag
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
- payload.offer(new StreamPayload(StreamPayloadType.DATA, getFlagsFromResource(INVALID_FLAG)));
+ payload.offer(new QueuePayload(QueuePayloadType.DATA, getFlagsFromResource(INVALID_FLAG), GetMetadataResponse.getDefaultInstance()));
});
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
@@ -53,7 +55,7 @@ public void connectorHandling() throws Exception {
// OK again for next payload
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
- payload.offer(new StreamPayload(StreamPayloadType.DATA, getFlagsFromResource(VALID_LONG)));
+ payload.offer(new QueuePayload(QueuePayloadType.DATA, getFlagsFromResource(VALID_LONG), GetMetadataResponse.getDefaultInstance()));
});
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
@@ -62,7 +64,7 @@ public void connectorHandling() throws Exception {
// ERROR is propagated correctly
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
- payload.offer(new StreamPayload(StreamPayloadType.ERROR, null));
+ payload.offer(new QueuePayload(QueuePayloadType.ERROR, null, GetMetadataResponse.getDefaultInstance()));
});
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
@@ -80,13 +82,13 @@ public void connectorHandling() throws Exception {
@Test
public void changedFlags() throws Exception {
final int maxDelay = 500;
- final BlockingQueue payload = new LinkedBlockingQueue<>();
+ final BlockingQueue payload = new LinkedBlockingQueue<>();
FlagStore store = new FlagStore(new MockConnector(payload), true);
store.init();
final BlockingQueue storageStateDTOS = store.getStateQueue();
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
- payload.offer(new StreamPayload(StreamPayloadType.DATA, getFlagsFromResource(VALID_SIMPLE)));
+ payload.offer(new QueuePayload(QueuePayloadType.DATA, getFlagsFromResource(VALID_SIMPLE), GetMetadataResponse.getDefaultInstance()));
});
// flags changed for first time
assertEquals(FlagParser.parseString(
@@ -94,7 +96,7 @@ public void changedFlags() throws Exception {
storageStateDTOS.take().getChangedFlagsKeys());
assertTimeoutPreemptively(Duration.ofMillis(maxDelay), ()-> {
- payload.offer(new StreamPayload(StreamPayloadType.DATA, getFlagsFromResource(VALID_LONG)));
+ payload.offer(new QueuePayload(QueuePayloadType.DATA, getFlagsFromResource(VALID_LONG), GetMetadataResponse.getDefaultInstance()));
});
Map expectedChangedFlags =
FlagParser.parseString(getFlagsFromResource(VALID_LONG),true);
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/MockConnector.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/MockConnector.java
index 495b69778..e94d7ed3f 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/MockConnector.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/MockConnector.java
@@ -1,18 +1,19 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;
+import java.util.concurrent.BlockingQueue;
+
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType;
+import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
import lombok.extern.slf4j.Slf4j;
-import java.util.concurrent.BlockingQueue;
-
@Slf4j
public class MockConnector implements Connector {
- private BlockingQueue mockQueue;
+ private BlockingQueue mockQueue;
- public MockConnector(final BlockingQueue mockQueue) {
+ public MockConnector(final BlockingQueue mockQueue) {
this.mockQueue = mockQueue;
}
@@ -20,13 +21,13 @@ public void init() {
// no-op
}
- public BlockingQueue getStream() {
+ public BlockingQueue getStream() {
return mockQueue;
}
public void shutdown() {
// Emit error mocking closed connection scenario
- if (!mockQueue.offer(new StreamPayload(StreamPayloadType.ERROR, "shutdown invoked"))) {
+ if (!mockQueue.offer(new QueuePayload(QueuePayloadType.ERROR, "shutdown invoked", GetMetadataResponse.getDefaultInstance()))) {
log.warn("Failed to offer shutdown status");
}
}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnectorTest.java
index 0f3b2f379..a2ad795c4 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnectorTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnectorTest.java
@@ -1,7 +1,7 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -31,16 +31,16 @@ void readAndExposeFeatureFlagsFromSource() throws IOException {
connector.init();
// then
- final BlockingQueue stream = connector.getStream();
- final StreamPayload[] payload = new StreamPayload[1];
+ final BlockingQueue stream = connector.getStream();
+ final QueuePayload[] payload = new QueuePayload[1];
assertNotNull(stream);
assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
payload[0] = stream.take();
});
- assertNotNull(payload[0].getData());
- assertEquals(StreamPayloadType.DATA, payload[0].getType());
+ assertNotNull(payload[0].getFlagData());
+ assertEquals(QueuePayloadType.DATA, payload[0].getType());
}
@Test
@@ -52,16 +52,16 @@ void emitErrorStateForInvalidPath() throws IOException {
connector.init();
// then
- final BlockingQueue stream = connector.getStream();
+ final BlockingQueue stream = connector.getStream();
// Must emit an error within considerable time
- final StreamPayload[] payload = new StreamPayload[1];
+ final QueuePayload[] payload = new QueuePayload[1];
assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
payload[0] = stream.take();
});
- assertNotNull(payload[0].getData());
- assertEquals(StreamPayloadType.ERROR, payload[0].getType());
+ assertNotNull(payload[0].getFlagData());
+ assertEquals(QueuePayloadType.ERROR, payload[0].getType());
}
@Test
@@ -80,15 +80,15 @@ void watchForFileUpdatesAndEmitThem() throws IOException {
connector.init();
// then
- final BlockingQueue stream = connector.getStream();
- final StreamPayload[] payload = new StreamPayload[1];
+ final BlockingQueue stream = connector.getStream();
+ final QueuePayload[] payload = new QueuePayload[1];
// first validate the initial payload
assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
payload[0] = stream.take();
});
- assertEquals(initial, payload[0].getData());
+ assertEquals(initial, payload[0].getFlagData());
// then update the flags
Files.write(updPath, updatedFlags.getBytes(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
@@ -98,7 +98,7 @@ void watchForFileUpdatesAndEmitThem() throws IOException {
payload[0] = stream.take();
});
- assertEquals(updatedFlags, payload[0].getData());
+ assertEquals(updatedFlags, payload[0].getFlagData());
}
}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnectorTest.java
index 10232f1c1..c19259807 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnectorTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnectorTest.java
@@ -1,12 +1,18 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.grpc;
+import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.convertProtobufMapToStructure;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.time.Duration;
@@ -14,12 +20,15 @@
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
+
+import com.google.protobuf.Struct;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
+import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType;
+import dev.openfeature.flagd.grpc.sync.FlagSyncServiceGrpc.FlagSyncServiceBlockingStub;
import dev.openfeature.flagd.grpc.sync.FlagSyncServiceGrpc.FlagSyncServiceStub;
+import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
import dev.openfeature.flagd.grpc.sync.Sync.SyncFlagsRequest;
import dev.openfeature.flagd.grpc.sync.Sync.SyncFlagsResponse;
@@ -32,21 +41,23 @@ public void connectionParameters() throws Throwable {
// given
final FlagdOptions options = FlagdOptions.builder()
.selector("selector")
+ .deadline(1337)
.build();
final GrpcStreamConnector connector = new GrpcStreamConnector(options);
final FlagSyncServiceStub stubMock = mockStubAndReturn(connector);
-
+ final FlagSyncServiceBlockingStub blockingStubMock = mockBlockingStubAndReturn(connector);
final SyncFlagsRequest[] request = new SyncFlagsRequest[1];
- Mockito.doAnswer(invocation -> {
+ doAnswer(invocation -> {
request[0] = invocation.getArgument(0, SyncFlagsRequest.class);
return null;
}).when(stubMock).syncFlags(any(), any());
// when
connector.init();
- verify(stubMock, Mockito.timeout(MAX_WAIT_MS.toMillis()).times(1)).syncFlags(any(), any());
+ verify(stubMock, timeout(MAX_WAIT_MS.toMillis()).times(1)).syncFlags(any(), any());
+ verify(blockingStubMock).withDeadlineAfter(1337, TimeUnit.MILLISECONDS);
// then
final SyncFlagsRequest flagsRequest = request[0];
@@ -57,12 +68,23 @@ public void connectionParameters() throws Throwable {
@Test
public void grpcConnectionStatus() throws Throwable {
// given
+ final String key = "key1";
+ final String val = "value1";
final GrpcStreamConnector connector = new GrpcStreamConnector(FlagdOptions.builder().build());
final FlagSyncServiceStub stubMock = mockStubAndReturn(connector);
+ final FlagSyncServiceBlockingStub blockingStubMock = mockBlockingStubAndReturn(connector);
+ final Struct metadata = Struct.newBuilder()
+ .putFields(key,
+ com.google.protobuf.Value.newBuilder().setStringValue(val).build())
+ .build();
+
+ when(blockingStubMock.withDeadlineAfter(anyLong(), any())).thenReturn(blockingStubMock);
+ when(blockingStubMock.getMetadata(any()))
+ .thenReturn(GetMetadataResponse.newBuilder().setMetadata(metadata).build());
final GrpcStreamHandler[] injectedHandler = new GrpcStreamHandler[1];
- Mockito.doAnswer(invocation -> {
+ doAnswer(invocation -> {
injectedHandler[0] = invocation.getArgument(1, GrpcStreamHandler.class);
return null;
}).when(stubMock).syncFlags(any(), any());
@@ -70,13 +92,14 @@ public void grpcConnectionStatus() throws Throwable {
// when
connector.init();
// verify and wait for initialization
- verify(stubMock, Mockito.timeout(MAX_WAIT_MS.toMillis()).times(1)).syncFlags(any(), any());
+ verify(stubMock, timeout(MAX_WAIT_MS.toMillis()).times(1)).syncFlags(any(), any());
+ verify(blockingStubMock).getMetadata(any());
// then
final GrpcStreamHandler grpcStreamHandler = injectedHandler[0];
assertNotNull(grpcStreamHandler);
- final BlockingQueue streamPayloads = connector.getStream();
+ final BlockingQueue streamPayloads = connector.getStream();
// accepted data
grpcStreamHandler.onNext(
@@ -84,8 +107,9 @@ public void grpcConnectionStatus() throws Throwable {
.build());
assertTimeoutPreemptively(MAX_WAIT_MS, () -> {
- StreamPayload payload = streamPayloads.take();
- assertEquals(StreamPayloadType.DATA, payload.getType());
+ QueuePayload payload = streamPayloads.take();
+ assertEquals(QueuePayloadType.DATA, payload.getType());
+ assertEquals(val ,convertProtobufMapToStructure(payload.getMetadataResponse().getMetadata().getFieldsMap()).asObjectMap().get(key));
});
// ping must be ignored
@@ -99,20 +123,26 @@ public void grpcConnectionStatus() throws Throwable {
.build());
assertTimeoutPreemptively(MAX_WAIT_MS, () -> {
- StreamPayload payload = streamPayloads.take();
- assertEquals(StreamPayloadType.DATA, payload.getType());
+ QueuePayload payload = streamPayloads.take();
+ assertEquals(QueuePayloadType.DATA, payload.getType());
});
}
@Test
+
public void listenerExitOnShutdown() throws Throwable {
// given
final GrpcStreamConnector connector = new GrpcStreamConnector(FlagdOptions.builder().build());
final FlagSyncServiceStub stubMock = mockStubAndReturn(connector);
-
+ final FlagSyncServiceBlockingStub blockingStubMock = mockBlockingStubAndReturn(connector);
final GrpcStreamHandler[] injectedHandler = new GrpcStreamHandler[1];
+ final Struct metadata = Struct.newBuilder().build();
- Mockito.doAnswer(invocation -> {
+ when(blockingStubMock.withDeadlineAfter(anyLong(), any())).thenReturn(blockingStubMock);
+ when(blockingStubMock.getMetadata(any()))
+ .thenReturn(GetMetadataResponse.newBuilder().setMetadata(metadata).build());
+ when(stubMock.withDeadlineAfter(anyLong(), any())).thenReturn(stubMock);
+ doAnswer(invocation -> {
injectedHandler[0] = invocation.getArgument(1, GrpcStreamHandler.class);
return null;
}).when(stubMock).syncFlags(any(), any());
@@ -120,7 +150,8 @@ public void listenerExitOnShutdown() throws Throwable {
// when
connector.init();
// verify and wait for initialization
- verify(stubMock, Mockito.timeout(MAX_WAIT_MS.toMillis()).times(1)).syncFlags(any(), any());
+ verify(stubMock, timeout(MAX_WAIT_MS.toMillis()).times(1)).syncFlags(any(), any());
+ verify(blockingStubMock).getMetadata(any());
// then
final GrpcStreamHandler grpcStreamHandler = injectedHandler[0];
@@ -132,8 +163,8 @@ public void listenerExitOnShutdown() throws Throwable {
grpcStreamHandler.onError(new Exception("Channel closed, exiting"));
assertTimeoutPreemptively(MAX_WAIT_MS, () -> {
- StreamPayload payload = connector.getStream().take();
- assertEquals(StreamPayloadType.ERROR, payload.getType());
+ QueuePayload payload = connector.getStream().take();
+ assertEquals(QueuePayloadType.ERROR, payload.getType());
});
// Validate mock calls & no more event propagation
@@ -154,11 +185,23 @@ private static FlagSyncServiceStub mockStubAndReturn(final GrpcStreamConnector c
final Field serviceStubField = GrpcStreamConnector.class.getDeclaredField("serviceStub");
serviceStubField.setAccessible(true);
- final FlagSyncServiceStub stubMock = Mockito.mock(FlagSyncServiceStub.class);
+ final FlagSyncServiceStub stubMock = mock(FlagSyncServiceStub.class);
serviceStubField.set(connector, stubMock);
return stubMock;
}
+ private static FlagSyncServiceBlockingStub mockBlockingStubAndReturn(final GrpcStreamConnector connector)
+ throws Throwable {
+ final Field blockingStubField = GrpcStreamConnector.class.getDeclaredField("serviceBlockingStub");
+ blockingStubField.setAccessible(true);
+
+ final FlagSyncServiceBlockingStub blockingStubMock = mock(FlagSyncServiceBlockingStub.class);
+
+ blockingStubField.set(connector, blockingStubMock);
+
+ return blockingStubMock;
+ }
+
}