Skip to content

Commit a330bd6

Browse files
chrfwowtoddbaert
andauthored
feat: Update in-process resolver to support flag metadata #1102 (#1122)
Signed-off-by: christian.lutnik <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent d2410c7 commit a330bd6

File tree

17 files changed

+495
-67
lines changed

17 files changed

+495
-67
lines changed

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java

+59-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
1111
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore;
1212
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage;
13+
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageQueryResult;
1314
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState;
1415
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange;
1516
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
@@ -25,6 +26,7 @@
2526
import dev.openfeature.sdk.Value;
2627
import dev.openfeature.sdk.exceptions.ParseError;
2728
import dev.openfeature.sdk.exceptions.TypeMismatchError;
29+
import java.util.Map;
2830
import java.util.function.Consumer;
2931
import java.util.function.Supplier;
3032
import lombok.extern.slf4j.Slf4j;
@@ -40,8 +42,8 @@ public class InProcessResolver implements Resolver {
4042
private final Consumer<ConnectionEvent> onConnectionEvent;
4143
private final Operator operator;
4244
private final long deadline;
43-
private final ImmutableMetadata metadata;
4445
private final Supplier<Boolean> connectedSupplier;
46+
private final String scope;
4547

4648
/**
4749
* Resolves flag values using
@@ -63,11 +65,7 @@ public InProcessResolver(
6365
this.onConnectionEvent = onConnectionEvent;
6466
this.operator = new Operator();
6567
this.connectedSupplier = connectedSupplier;
66-
this.metadata = options.getSelector() == null
67-
? null
68-
: ImmutableMetadata.builder()
69-
.addString("scope", options.getSelector())
70-
.build();
68+
this.scope = options.getSelector();
7169
}
7270

7371
/**
@@ -167,13 +165,15 @@ static Connector getConnector(final FlagdOptions options, Consumer<ConnectionEve
167165
}
168166

169167
private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationContext ctx) {
170-
final FeatureFlag flag = flagStore.getFlag(key);
168+
final StorageQueryResult storageQueryResult = flagStore.getFlag(key);
169+
final FeatureFlag flag = storageQueryResult.getFeatureFlag();
171170

172171
// missing flag
173172
if (flag == null) {
174173
return ProviderEvaluation.<T>builder()
175174
.errorMessage("flag: " + key + " not found")
176175
.errorCode(ErrorCode.FLAG_NOT_FOUND)
176+
.flagMetadata(getFlagMetadata(storageQueryResult))
177177
.build();
178178
}
179179

@@ -182,6 +182,7 @@ private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationC
182182
return ProviderEvaluation.<T>builder()
183183
.errorMessage("flag: " + key + " is disabled")
184184
.errorCode(ErrorCode.FLAG_NOT_FOUND)
185+
.flagMetadata(getFlagMetadata(storageQueryResult))
185186
.build();
186187
}
187188

@@ -228,13 +229,59 @@ private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationC
228229
throw new TypeMismatchError(message);
229230
}
230231

231-
final ProviderEvaluation.ProviderEvaluationBuilder<T> evaluationBuilder = ProviderEvaluation.<T>builder()
232+
return ProviderEvaluation.<T>builder()
232233
.value((T) value)
233234
.variant(resolvedVariant)
234-
.reason(reason);
235+
.reason(reason)
236+
.flagMetadata(getFlagMetadata(storageQueryResult))
237+
.build();
238+
}
239+
240+
private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) {
241+
ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder();
242+
for (Map.Entry<String, Object> entry :
243+
storageQueryResult.getFlagSetMetadata().entrySet()) {
244+
addEntryToMetadataBuilder(metadataBuilder, entry.getKey(), entry.getValue());
245+
}
235246

236-
return this.metadata == null
237-
? evaluationBuilder.build()
238-
: evaluationBuilder.flagMetadata(this.metadata).build();
247+
if (scope != null) {
248+
metadataBuilder.addString("scope", scope);
249+
}
250+
251+
FeatureFlag flag = storageQueryResult.getFeatureFlag();
252+
if (flag != null) {
253+
for (Map.Entry<String, Object> entry : flag.getMetadata().entrySet()) {
254+
addEntryToMetadataBuilder(metadataBuilder, entry.getKey(), entry.getValue());
255+
}
256+
}
257+
258+
return metadataBuilder.build();
259+
}
260+
261+
private void addEntryToMetadataBuilder(
262+
ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder, String key, Object value) {
263+
if (value instanceof Number) {
264+
if (value instanceof Long) {
265+
metadataBuilder.addLong(key, (Long) value);
266+
return;
267+
} else if (value instanceof Double) {
268+
metadataBuilder.addDouble(key, (Double) value);
269+
return;
270+
} else if (value instanceof Integer) {
271+
metadataBuilder.addInteger(key, (Integer) value);
272+
return;
273+
} else if (value instanceof Float) {
274+
metadataBuilder.addFloat(key, (Float) value);
275+
return;
276+
}
277+
} else if (value instanceof Boolean) {
278+
metadataBuilder.addBoolean(key, (Boolean) value);
279+
return;
280+
} else if (value instanceof String) {
281+
metadataBuilder.addString(key, (String) value);
282+
return;
283+
}
284+
throw new IllegalArgumentException(
285+
"The type of the Metadata entry with key " + key + " and value " + value + " is not supported");
239286
}
240287
}

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.annotation.JsonProperty;
66
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
77
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
8+
import java.util.HashMap;
89
import java.util.Map;
910
import lombok.EqualsAndHashCode;
1011
import lombok.Getter;
@@ -23,18 +24,34 @@ public class FeatureFlag {
2324
private final String defaultVariant;
2425
private final Map<String, Object> variants;
2526
private final String targeting;
27+
private final Map<String, Object> metadata;
2628

2729
/** Construct a flagd feature flag. */
2830
@JsonCreator
2931
public FeatureFlag(
3032
@JsonProperty("state") String state,
3133
@JsonProperty("defaultVariant") String defaultVariant,
3234
@JsonProperty("variants") Map<String, Object> variants,
33-
@JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting) {
35+
@JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting,
36+
@JsonProperty("metadata") Map<String, Object> metadata) {
3437
this.state = state;
3538
this.defaultVariant = defaultVariant;
3639
this.variants = variants;
3740
this.targeting = targeting;
41+
if (metadata == null) {
42+
this.metadata = new HashMap<>();
43+
} else {
44+
this.metadata = metadata;
45+
}
46+
}
47+
48+
/** Construct a flagd feature flag. */
49+
public FeatureFlag(String state, String defaultVariant, Map<String, Object> variants, String targeting) {
50+
this.state = state;
51+
this.defaultVariant = defaultVariant;
52+
this.variants = variants;
53+
this.targeting = targeting;
54+
this.metadata = new HashMap<>();
3855
}
3956

4057
/** Get targeting rule of the flag. */

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package dev.openfeature.contrib.providers.flagd.resolver.process.model;
22

33
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.core.JsonProcessingException;
45
import com.fasterxml.jackson.core.TreeNode;
6+
import com.fasterxml.jackson.core.type.TypeReference;
57
import com.fasterxml.jackson.databind.ObjectMapper;
68
import com.networknt.schema.JsonSchema;
79
import com.networknt.schema.JsonSchemaFactory;
@@ -24,6 +26,7 @@
2426
justification = "Feature flag comes as a Json configuration, hence they must be exposed")
2527
public class FlagParser {
2628
private static final String FLAG_KEY = "flags";
29+
private static final String METADATA_KEY = "metadata";
2730
private static final String EVALUATOR_KEY = "$evaluators";
2831
private static final String REPLACER_FORMAT = "\"\\$ref\":(\\s)*\"%s\"";
2932
private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -50,8 +53,7 @@ private FlagParser() {}
5053
}
5154

5255
/** Parse {@link String} for feature flags. */
53-
public static Map<String, FeatureFlag> parseString(final String configuration, boolean throwIfInvalid)
54-
throws IOException {
56+
public static ParsingResult parseString(final String configuration, boolean throwIfInvalid) throws IOException {
5557
if (SCHEMA_VALIDATOR != null) {
5658
try (JsonParser parser = MAPPER.createParser(configuration)) {
5759
Set<ValidationMessage> validationMessages = SCHEMA_VALIDATOR.validate(parser.readValueAsTree());
@@ -69,10 +71,12 @@ public static Map<String, FeatureFlag> parseString(final String configuration, b
6971
final String transposedConfiguration = transposeEvaluators(configuration);
7072

7173
final Map<String, FeatureFlag> flagMap = new HashMap<>();
72-
74+
final Map<String, Object> flagSetMetadata;
7375
try (JsonParser parser = MAPPER.createParser(transposedConfiguration)) {
7476
final TreeNode treeNode = parser.readValueAsTree();
7577
final TreeNode flagNode = treeNode.get(FLAG_KEY);
78+
final TreeNode metadataNode = treeNode.get(METADATA_KEY);
79+
flagSetMetadata = parseMetadata(metadataNode);
7680

7781
if (flagNode == null) {
7882
throw new IllegalArgumentException("No flag configurations found in the payload");
@@ -85,7 +89,16 @@ public static Map<String, FeatureFlag> parseString(final String configuration, b
8589
}
8690
}
8791

88-
return flagMap;
92+
return new ParsingResult(flagMap, flagSetMetadata);
93+
}
94+
95+
private static Map<String, Object> parseMetadata(TreeNode metadataNode) throws JsonProcessingException {
96+
if (metadataNode == null) {
97+
return new HashMap<>();
98+
}
99+
100+
TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {};
101+
return MAPPER.treeToValue(metadataNode, typeRef);
89102
}
90103

91104
private static String transposeEvaluators(final String configuration) throws IOException {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.process.model;
2+
3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4+
import java.util.Map;
5+
import lombok.Getter;
6+
7+
/**
8+
* The result of the parsing of a json string containing feature flag definitions.
9+
*/
10+
@Getter
11+
@SuppressFBWarnings(
12+
value = {"EI_EXPOSE_REP"},
13+
justification = "Feature flag comes as a Json configuration, hence they must be exposed")
14+
public class ParsingResult {
15+
private final Map<String, FeatureFlag> flags;
16+
private final Map<String, Object> flagSetMetadata;
17+
18+
public ParsingResult(Map<String, FeatureFlag> flags, Map<String, Object> flagSetMetadata) {
19+
this.flags = flags;
20+
this.flagSetMetadata = flagSetMetadata;
21+
}
22+
}

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java

+19-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
66
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FlagParser;
7+
import dev.openfeature.contrib.providers.flagd.resolver.process.model.ParsingResult;
78
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
89
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
910
import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse;
@@ -35,6 +36,7 @@ public class FlagStore implements Storage {
3536
private final AtomicBoolean shutdown = new AtomicBoolean(false);
3637
private final BlockingQueue<StorageStateChange> stateBlockingQueue = new LinkedBlockingQueue<>(1);
3738
private final Map<String, FeatureFlag> flags = new HashMap<>();
39+
private final Map<String, Object> flagSetMetadata = new HashMap<>();
3840

3941
private final Connector connector;
4042
private final boolean throwIfInvalid;
@@ -49,6 +51,7 @@ public FlagStore(final Connector connector, final boolean throwIfInvalid) {
4951
}
5052

5153
/** Initialize storage layer. */
54+
@Override
5255
public void init() throws Exception {
5356
connector.init();
5457
Thread streamer = new Thread(() -> {
@@ -68,6 +71,7 @@ public void init() throws Exception {
6871
*
6972
* @throws InterruptedException if stream can't be closed within deadline.
7073
*/
74+
@Override
7175
public void shutdown() throws InterruptedException {
7276
if (shutdown.getAndSet(true)) {
7377
return;
@@ -76,17 +80,23 @@ public void shutdown() throws InterruptedException {
7680
connector.shutdown();
7781
}
7882

79-
/** Retrieve flag for the given key. */
80-
public FeatureFlag getFlag(final String key) {
83+
/** Retrieve flag for the given key and the flag set metadata. */
84+
@Override
85+
public StorageQueryResult getFlag(final String key) {
8186
readLock.lock();
87+
FeatureFlag flag;
88+
Map<String, Object> metadata;
8289
try {
83-
return flags.get(key);
90+
flag = flags.get(key);
91+
metadata = new HashMap<>(flagSetMetadata);
8492
} finally {
8593
readLock.unlock();
8694
}
95+
return new StorageQueryResult(flag, metadata);
8796
}
8897

8998
/** Retrieve blocking queue to check storage status. */
99+
@Override
90100
public BlockingQueue<StorageStateChange> getStateQueue() {
91101
return stateBlockingQueue;
92102
}
@@ -100,14 +110,18 @@ private void streamerListener(final Connector connector) throws InterruptedExcep
100110
case DATA:
101111
try {
102112
List<String> changedFlagsKeys;
103-
Map<String, FeatureFlag> flagMap =
104-
FlagParser.parseString(payload.getFlagData(), throwIfInvalid);
113+
ParsingResult parsingResult = FlagParser.parseString(payload.getFlagData(), throwIfInvalid);
114+
Map<String, FeatureFlag> flagMap = parsingResult.getFlags();
115+
Map<String, Object> flagSetMetadataMap = parsingResult.getFlagSetMetadata();
116+
105117
Structure metadata = parseSyncMetadata(payload.getMetadataResponse());
106118
writeLock.lock();
107119
try {
108120
changedFlagsKeys = getChangedFlagsKeys(flagMap);
109121
flags.clear();
110122
flags.putAll(flagMap);
123+
flagSetMetadata.clear();
124+
flagSetMetadata.putAll(flagSetMetadataMap);
111125
} finally {
112126
writeLock.unlock();
113127
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;
22

3-
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
43
import java.util.concurrent.BlockingQueue;
54

65
/** Storage abstraction for resolver. */
@@ -9,7 +8,7 @@ public interface Storage {
98

109
void shutdown() throws InterruptedException;
1110

12-
FeatureFlag getFlag(final String key);
11+
StorageQueryResult getFlag(final String key);
1312

1413
BlockingQueue<StorageStateChange> getStateQueue();
1514
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;
2+
3+
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
4+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
5+
import java.util.Map;
6+
import lombok.Getter;
7+
8+
/**
9+
* To be returned by the storage when a flag is queried. Contains the flag (iff a flag associated with the given key
10+
* exists, null otherwise) and flag set metadata
11+
*/
12+
@Getter
13+
@SuppressFBWarnings(
14+
value = {"EI_EXPOSE_REP"},
15+
justification = "The storage provides access to both feature flags and flag set metadata")
16+
public class StorageQueryResult {
17+
private final FeatureFlag featureFlag;
18+
private final Map<String, Object> flagSetMetadata;
19+
20+
public StorageQueryResult(FeatureFlag featureFlag, Map<String, Object> flagSetMetadata) {
21+
this.featureFlag = featureFlag;
22+
this.flagSetMetadata = flagSetMetadata;
23+
}
24+
}

0 commit comments

Comments
 (0)