Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose sync-metadata, call RPC with (re)connect #967

Merged
merged 12 commits into from
Sep 30, 2024
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() {
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
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;
toddbaert marked this conversation as resolved.
Show resolved Hide resolved

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