Skip to content

Commit

Permalink
feat: expose sync-metadata, call RPC with (re)connect (#967)
Browse files Browse the repository at this point in the history
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Co-authored-by: Simon Schrottner <simon.schrottner@dynatrace.com>
  • Loading branch information
toddbaert and aepfli authored Sep 30, 2024
1 parent a58a64e commit 61bb726
Show file tree
Hide file tree
Showing 30 changed files with 1,656 additions and 1,230 deletions.
7 changes: 7 additions & 0 deletions providers/flagd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion providers/flagd/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</properties>

<name>flagd</name>
<description>FlagD provider for Java</description>
<description>flagd provider for Java</description>
<url>https://openfeature.dev</url>

<developers>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, Object> syncMetadata = Collections.emptyMap();

private EvaluationContext evaluationContext;

Expand All @@ -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(
Expand Down Expand Up @@ -117,6 +120,19 @@ public ProviderEvaluation<Value> 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<String, Object> getSyncMetadata() {
return Collections.unmodifiableMap(syncMetadata);
}

private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) {
if (this.evaluationContext != null) {
return evaluationContext.merge(clientCallCtx);
Expand All @@ -129,15 +145,16 @@ private boolean isConnected() {
return this.connected;
}

private void onResolverConnectionChanged(boolean newConnectedState, List<String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> flagsChanged;
private final Map<String, Object> 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<String> 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<String, Object> syncMetadata) {
this(connected, Collections.emptyList(), syncMetadata);
}

/**
* Get changed flags.
*
* @return an unmodifiable view of the changed flags
*/
public List<String> getFlagsChanged() {
return Collections.unmodifiableList(flagsChanged);
}

/**
* Get changed sync metadata.
*
* @return an unmodifiable view of the sync metadata
*/
public Map<String, Object> getSyncMetadata() {
return Collections.unmodifiableMap(syncMetadata);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Value> 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<String, Value> map) {
Map<String, com.google.protobuf.Value> 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<String, com.google.protobuf.Value> map) {
return new Value(convertProtobufMapToStructure(map));
}

/**
* Convert protobuf map with {@link com.google.protobuf.Value} to OpenFeature
* map.
*/
public static Structure convertProtobufMapToStructure(Map<String, com.google.protobuf.Value> map) {
Map<String, Value> 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<Value> 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 <T> type
* @param message protobuf message
* @param name field name
* @return field value
*/
public static <T> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
Loading

0 comments on commit 61bb726

Please sign in to comment.