Skip to content

Commit 61bb726

Browse files
toddbaertaepfli
andauthored
feat: expose sync-metadata, call RPC with (re)connect (#967)
Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Simon Schrottner <[email protected]>
1 parent a58a64e commit 61bb726

30 files changed

+1656
-1230
lines changed

providers/flagd/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ FlagdProvider flagdProvider = new FlagdProvider(
4747

4848
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).
4949

50+
#### Sync-metadata
51+
52+
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).
53+
The value is updated with every (re)connection to the sync implementation.
54+
This can be used to enrich evaluations with such data.
55+
If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map.
56+
5057
#### Offline mode
5158

5259
In-process resolvers can also work in an offline mode.

providers/flagd/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</properties>
2020

2121
<name>flagd</name>
22-
<description>FlagD provider for Java</description>
22+
<description>flagd provider for Java</description>
2323
<url>https://openfeature.dev</url>
2424

2525
<developers>

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package dev.openfeature.contrib.providers.flagd;
22

3-
import java.util.List;
3+
import java.util.Collections;
4+
import java.util.Map;
45

56
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
7+
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
68
import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver;
79
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
810
import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver;
@@ -20,10 +22,11 @@
2022
@Slf4j
2123
@SuppressWarnings({ "PMD.TooManyStaticImports", "checkstyle:NoFinalizer" })
2224
public class FlagdProvider extends EventProvider {
23-
private static final String FLAGD_PROVIDER = "flagD Provider";
25+
private static final String FLAGD_PROVIDER = "flagd";
2426
private final Resolver flagResolver;
2527
private volatile boolean initialized = false;
2628
private volatile boolean connected = false;
29+
private volatile Map<String, Object> syncMetadata = Collections.emptyMap();
2730

2831
private EvaluationContext evaluationContext;
2932

@@ -47,13 +50,13 @@ public FlagdProvider(final FlagdOptions options) {
4750
switch (options.getResolverType().asString()) {
4851
case Config.RESOLVER_IN_PROCESS:
4952
this.flagResolver = new InProcessResolver(options, this::isConnected,
50-
this::onResolverConnectionChanged);
53+
this::onConnectionEvent);
5154
break;
5255
case Config.RESOLVER_RPC:
5356
this.flagResolver = new GrpcResolver(options,
5457
new Cache(options.getCacheType(), options.getMaxCacheSize()),
5558
this::isConnected,
56-
this::onResolverConnectionChanged);
59+
this::onConnectionEvent);
5760
break;
5861
default:
5962
throw new IllegalStateException(
@@ -117,6 +120,19 @@ public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultVa
117120
return this.flagResolver.objectEvaluation(key, defaultValue, mergeContext(ctx));
118121
}
119122

123+
/**
124+
* An unmodifiable view of an object map representing the latest result of the
125+
* SyncMetadata.
126+
* Set on initial connection and updated with every reconnection.
127+
* see:
128+
* https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata
129+
*
130+
* @return Object map representing sync metadata
131+
*/
132+
protected Map<String, Object> getSyncMetadata() {
133+
return Collections.unmodifiableMap(syncMetadata);
134+
}
135+
120136
private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) {
121137
if (this.evaluationContext != null) {
122138
return evaluationContext.merge(clientCallCtx);
@@ -129,15 +145,16 @@ private boolean isConnected() {
129145
return this.connected;
130146
}
131147

132-
private void onResolverConnectionChanged(boolean newConnectedState, List<String> changedFlagKeys) {
148+
private void onConnectionEvent(ConnectionEvent connectionEvent) {
133149
boolean previous = connected;
134-
boolean current = newConnectedState;
135-
this.connected = newConnectedState;
150+
boolean current = connected = connectionEvent.isConnected();
151+
syncMetadata = connectionEvent.getSyncMetadata();
136152

137153
// configuration changed
138154
if (initialized && previous && current) {
139155
log.debug("Configuration changed");
140-
ProviderEventDetails details = ProviderEventDetails.builder().flagsChanged(changedFlagKeys)
156+
ProviderEventDetails details = ProviderEventDetails.builder()
157+
.flagsChanged(connectionEvent.getFlagsChanged())
141158
.message("configuration changed").build();
142159
this.emitProviderConfigurationChanged(details);
143160
return;

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import dev.openfeature.sdk.Value;
66

77
/**
8-
* A generic flag resolving contract for flagd.
8+
* Abstraction that resolves flag values in from some source.
99
*/
1010
public interface Resolver {
1111
void init() throws Exception;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
import java.util.Map;
6+
7+
import lombok.AllArgsConstructor;
8+
import lombok.Getter;
9+
10+
/**
11+
* Event payload for a
12+
* {@link dev.openfeature.contrib.providers.flagd.resolver.Resolver} connection
13+
* state change event.
14+
*/
15+
@AllArgsConstructor
16+
public class ConnectionEvent {
17+
@Getter
18+
private final boolean connected;
19+
private final List<String> flagsChanged;
20+
private final Map<String, Object> syncMetadata;
21+
22+
/**
23+
* Construct a new ConnectionEvent.
24+
*
25+
* @param connected status of the connection
26+
*/
27+
public ConnectionEvent(boolean connected) {
28+
this(connected, Collections.emptyList(), Collections.emptyMap());
29+
}
30+
31+
/**
32+
* Construct a new ConnectionEvent.
33+
*
34+
* @param connected status of the connection
35+
* @param flagsChanged list of flags changed
36+
*/
37+
public ConnectionEvent(boolean connected, List<String> flagsChanged) {
38+
this(connected, flagsChanged, Collections.emptyMap());
39+
}
40+
41+
/**
42+
* Construct a new ConnectionEvent.
43+
*
44+
* @param connected status of the connection
45+
* @param syncMetadata sync.getMetadata
46+
*/
47+
public ConnectionEvent(boolean connected, Map<String, Object> syncMetadata) {
48+
this(connected, Collections.emptyList(), syncMetadata);
49+
}
50+
51+
/**
52+
* Get changed flags.
53+
*
54+
* @return an unmodifiable view of the changed flags
55+
*/
56+
public List<String> getFlagsChanged() {
57+
return Collections.unmodifiableList(flagsChanged);
58+
}
59+
60+
/**
61+
* Get changed sync metadata.
62+
*
63+
* @return an unmodifiable view of the sync metadata
64+
*/
65+
public Map<String, Object> getSyncMetadata() {
66+
return Collections.unmodifiableMap(syncMetadata);
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common;
2+
3+
import java.util.HashMap;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.stream.Collectors;
7+
8+
import com.google.protobuf.Descriptors;
9+
import com.google.protobuf.ListValue;
10+
import com.google.protobuf.Message;
11+
import com.google.protobuf.NullValue;
12+
import com.google.protobuf.Struct;
13+
14+
import dev.openfeature.sdk.EvaluationContext;
15+
import dev.openfeature.sdk.MutableStructure;
16+
import dev.openfeature.sdk.Structure;
17+
import dev.openfeature.sdk.Value;
18+
19+
/**
20+
* gRPC type conversion utils.
21+
*/
22+
public class Convert {
23+
/**
24+
* Recursively convert protobuf structure to openfeature value.
25+
*/
26+
public static Value convertObjectResponse(Struct protobuf) {
27+
return convertProtobufMap(protobuf.getFieldsMap());
28+
}
29+
30+
/**
31+
* Recursively convert the Evaluation context to a protobuf structure.
32+
*/
33+
public static Struct convertContext(EvaluationContext ctx) {
34+
Map<String, Value> ctxMap = ctx.asMap();
35+
// asMap() does not provide explicitly set targeting key (ex:- new
36+
// ImmutableContext("TargetingKey") ).
37+
// Hence, we add this explicitly here for targeting rule processing.
38+
ctxMap.put("targetingKey", new Value(ctx.getTargetingKey()));
39+
40+
return convertMap(ctxMap).getStructValue();
41+
}
42+
43+
/**
44+
* Convert any openfeature value to a protobuf value.
45+
*/
46+
public static com.google.protobuf.Value convertAny(Value value) {
47+
if (value.isList()) {
48+
return convertList(value.asList());
49+
} else if (value.isStructure()) {
50+
return convertMap(value.asStructure().asMap());
51+
} else {
52+
return convertPrimitive(value);
53+
}
54+
}
55+
56+
/**
57+
* Convert any protobuf value to {@link Value}.
58+
*/
59+
public static Value convertAny(com.google.protobuf.Value protobuf) {
60+
if (protobuf.hasListValue()) {
61+
return convertList(protobuf.getListValue());
62+
} else if (protobuf.hasStructValue()) {
63+
return convertProtobufMap(protobuf.getStructValue().getFieldsMap());
64+
} else {
65+
return convertPrimitive(protobuf);
66+
}
67+
}
68+
69+
/**
70+
* Convert OpenFeature map to protobuf {@link com.google.protobuf.Value}.
71+
*/
72+
public static com.google.protobuf.Value convertMap(Map<String, Value> map) {
73+
Map<String, com.google.protobuf.Value> values = new HashMap<>();
74+
75+
map.keySet().forEach((String key) -> {
76+
Value value = map.get(key);
77+
values.put(key, convertAny(value));
78+
});
79+
Struct struct = Struct.newBuilder()
80+
.putAllFields(values).build();
81+
return com.google.protobuf.Value.newBuilder().setStructValue(struct).build();
82+
}
83+
84+
/**
85+
* Convert protobuf map with {@link com.google.protobuf.Value} to OpenFeature
86+
* map.
87+
*/
88+
public static Value convertProtobufMap(Map<String, com.google.protobuf.Value> map) {
89+
return new Value(convertProtobufMapToStructure(map));
90+
}
91+
92+
/**
93+
* Convert protobuf map with {@link com.google.protobuf.Value} to OpenFeature
94+
* map.
95+
*/
96+
public static Structure convertProtobufMapToStructure(Map<String, com.google.protobuf.Value> map) {
97+
Map<String, Value> values = new HashMap<>();
98+
99+
map.keySet().forEach((String key) -> {
100+
com.google.protobuf.Value value = map.get(key);
101+
values.put(key, convertAny(value));
102+
});
103+
return new MutableStructure(values);
104+
}
105+
106+
/**
107+
* Convert OpenFeature list to protobuf {@link com.google.protobuf.Value}.
108+
*/
109+
public static com.google.protobuf.Value convertList(List<Value> values) {
110+
ListValue list = ListValue.newBuilder()
111+
.addAllValues(values.stream()
112+
.map(v -> convertAny(v)).collect(Collectors.toList()))
113+
.build();
114+
return com.google.protobuf.Value.newBuilder().setListValue(list).build();
115+
}
116+
117+
/**
118+
* Convert protobuf list to OpenFeature {@link com.google.protobuf.Value}.
119+
*/
120+
public static Value convertList(ListValue protobuf) {
121+
return new Value(protobuf.getValuesList().stream().map(p -> convertAny(p)).collect(Collectors.toList()));
122+
}
123+
124+
/**
125+
* Convert OpenFeature {@link Value} to protobuf
126+
* {@link com.google.protobuf.Value}.
127+
*/
128+
public static com.google.protobuf.Value convertPrimitive(Value value) {
129+
com.google.protobuf.Value.Builder builder = com.google.protobuf.Value.newBuilder();
130+
131+
if (value.isBoolean()) {
132+
builder.setBoolValue(value.asBoolean());
133+
} else if (value.isString()) {
134+
builder.setStringValue(value.asString());
135+
} else if (value.isNumber()) {
136+
builder.setNumberValue(value.asDouble());
137+
} else {
138+
builder.setNullValue(NullValue.NULL_VALUE);
139+
}
140+
return builder.build();
141+
}
142+
143+
/**
144+
* Convert protobuf {@link com.google.protobuf.Value} to OpenFeature
145+
* {@link Value}.
146+
*/
147+
public static Value convertPrimitive(com.google.protobuf.Value protobuf) {
148+
final Value value;
149+
if (protobuf.hasBoolValue()) {
150+
value = new Value(protobuf.getBoolValue());
151+
} else if (protobuf.hasStringValue()) {
152+
value = new Value(protobuf.getStringValue());
153+
} else if (protobuf.hasNumberValue()) {
154+
value = new Value(protobuf.getNumberValue());
155+
} else {
156+
value = new Value();
157+
}
158+
159+
return value;
160+
}
161+
162+
/**
163+
* Get the specified protobuf field from the message.
164+
*
165+
* @param <T> type
166+
* @param message protobuf message
167+
* @param name field name
168+
* @return field value
169+
*/
170+
public static <T> T getField(Message message, String name) {
171+
return (T) message.getField(getFieldDescriptor(message, name));
172+
}
173+
174+
/**
175+
* Get the specified protobuf field descriptor from the message.
176+
*
177+
* @param message protobuf message
178+
* @param name field name
179+
* @return field descriptor
180+
*/
181+
public static Descriptors.FieldDescriptor getFieldDescriptor(Message message, String name) {
182+
return message.getDescriptorForType().findFieldByName(name);
183+
}
184+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.grpc;
2+
3+
/**
4+
* Constants for evaluation proto.
5+
*/
6+
public class Constants {
7+
public static final String CONFIGURATION_CHANGE = "configuration_change";
8+
public static final String PROVIDER_READY = "provider_ready";
9+
public static final String FLAGS_KEY = "flags";
10+
}

0 commit comments

Comments
 (0)