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: Update in-process resolver to support flag metadata #1102 #1122

Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d127cb5
feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 2, 2025
0bedec3
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 2, 2025
319ee49
Merge branch 'main' into feat/Update-in-process-resolver-to-support-f…
chrfwow Jan 2, 2025
c08e7bc
feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 2, 2025
4c6393a
Merge remote-tracking branch 'origin/feat/Update-in-process-resolver-…
chrfwow Jan 7, 2025
da9e92b
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 9, 2025
18219c0
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 9, 2025
af0006c
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 9, 2025
f8e69aa
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 9, 2025
72d1fbc
Merge branch 'main' into feat/Update-in-process-resolver-to-support-f…
chrfwow Jan 9, 2025
b495c6b
Merge branch 'main' into feat/Update-in-process-resolver-to-support-f…
toddbaert Jan 9, 2025
65a508c
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 10, 2025
9b49abc
Merge remote-tracking branch 'origin/feat/Update-in-process-resolver-…
chrfwow Jan 10, 2025
a8a2cd6
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 10, 2025
53f570f
Merge branch 'main' into feat/Update-in-process-resolver-to-support-f…
toddbaert Jan 13, 2025
1579eff
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 14, 2025
0551684
fixup! feat: Update in-process resolver to support flag metadata #1102
chrfwow Jan 14, 2025
242b262
fixup: merge conflicts
toddbaert Jan 14, 2025
b0868e0
fixup: more conflicts
toddbaert Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageQueryResult;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file.FileConnector;
Expand All @@ -24,6 +25,7 @@
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.ParseError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -39,8 +41,8 @@ public class InProcessResolver implements Resolver {
private final Consumer<ConnectionEvent> onConnectionEvent;
private final Operator operator;
private final long deadline;
private final ImmutableMetadata metadata;
private final Supplier<Boolean> connectedSupplier;
private final String scope;

/**
* Resolves flag values using
Expand All @@ -62,11 +64,7 @@ public InProcessResolver(
this.onConnectionEvent = onConnectionEvent;
this.operator = new Operator();
this.connectedSupplier = connectedSupplier;
this.metadata = options.getSelector() == null
? null
: ImmutableMetadata.builder()
.addString("scope", options.getSelector())
.build();
this.scope = options.getSelector();
}

/**
Expand Down Expand Up @@ -171,13 +169,15 @@ static Connector getConnector(final FlagdOptions options) {
}

private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationContext ctx) {
final FeatureFlag flag = flagStore.getFlag(key);
final StorageQueryResult storageQueryResult = flagStore.getFlag(key);
final FeatureFlag flag = storageQueryResult.getFeatureFlag();

// missing flag
if (flag == null) {
return ProviderEvaluation.<T>builder()
.errorMessage("flag: " + key + " not found")
.errorCode(ErrorCode.FLAG_NOT_FOUND)
.flagMetadata(getFlagMetadata(storageQueryResult))
.build();
}

Expand All @@ -186,6 +186,7 @@ private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationC
return ProviderEvaluation.<T>builder()
.errorMessage("flag: " + key + " is disabled")
.errorCode(ErrorCode.FLAG_NOT_FOUND)
.flagMetadata(getFlagMetadata(storageQueryResult))
.build();
}

Expand Down Expand Up @@ -232,13 +233,59 @@ private <T> ProviderEvaluation<T> resolve(Class<T> type, String key, EvaluationC
throw new TypeMismatchError(message);
}

final ProviderEvaluation.ProviderEvaluationBuilder<T> evaluationBuilder = ProviderEvaluation.<T>builder()
return ProviderEvaluation.<T>builder()
.value((T) value)
.variant(resolvedVariant)
.reason(reason);
.reason(reason)
.flagMetadata(getFlagMetadata(storageQueryResult))
.build();
}

private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) {
ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder();
for (Map.Entry<String, Object> entry :
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
storageQueryResult.getGlobalFlagMetadata().entrySet()) {
addEntryToMetadataBuilder(metadataBuilder, entry.getKey(), entry.getValue());
}

return this.metadata == null
? evaluationBuilder.build()
: evaluationBuilder.flagMetadata(this.metadata).build();
if (scope != null) {
metadataBuilder.addString("scope", scope);
}

FeatureFlag flag = storageQueryResult.getFeatureFlag();
if (flag != null) {
for (Map.Entry<String, Object> entry : flag.getMetadata().entrySet()) {
addEntryToMetadataBuilder(metadataBuilder, entry.getKey(), entry.getValue());
}
}

return metadataBuilder.build();
}

private void addEntryToMetadataBuilder(
ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder, String key, Object value) {
if (value instanceof Number) {
if (value instanceof Long) {
metadataBuilder.addLong(key, (Long) value);
return;
} else if (value instanceof Double) {
metadataBuilder.addDouble(key, (Double) value);
return;
} else if (value instanceof Integer) {
metadataBuilder.addInteger(key, (Integer) value);
return;
} else if (value instanceof Float) {
metadataBuilder.addFloat(key, (Float) value);
return;
}
} else if (value instanceof Boolean) {
metadataBuilder.addBoolean(key, (Boolean) value);
return;
} else if (value instanceof String) {
metadataBuilder.addString(key, (String) value);
return;
}
throw new IllegalArgumentException(
"The type of the Metadata entry with key " + key + " and value " + value + " is not supported");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.HashMap;
import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Getter;
Expand All @@ -23,18 +24,34 @@ public class FeatureFlag {
private final String defaultVariant;
private final Map<String, Object> variants;
private final String targeting;
private final Map<String, Object> metadata;

/** Construct a flagd feature flag. */
@JsonCreator
public FeatureFlag(
@JsonProperty("state") String state,
@JsonProperty("defaultVariant") String defaultVariant,
@JsonProperty("variants") Map<String, Object> variants,
@JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting) {
@JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting,
@JsonProperty("metadata") Map<String, Object> metadata) {
this.state = state;
this.defaultVariant = defaultVariant;
this.variants = variants;
this.targeting = targeting;
if (metadata == null) {
this.metadata = new HashMap<>();
} else {
this.metadata = metadata;
}
}

/** Construct a flagd feature flag. */
public FeatureFlag(String state, String defaultVariant, Map<String, Object> variants, String targeting) {
this.state = state;
this.defaultVariant = defaultVariant;
this.variants = variants;
this.targeting = targeting;
this.metadata = new HashMap<>();
}

/** Get targeting rule of the flag. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.model;

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

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

final Map<String, FeatureFlag> flagMap = new HashMap<>();

final Map<String, Object> metadata;
try (JsonParser parser = MAPPER.createParser(transposedConfiguration)) {
final TreeNode treeNode = parser.readValueAsTree();
final TreeNode flagNode = treeNode.get(FLAG_KEY);
final TreeNode metadataNode = treeNode.get(METADATA_KEY);
metadata = parseMetadata(metadataNode);

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

return flagMap;
return new ParsingResult(flagMap, metadata);
}

private static Map<String, Object> parseMetadata(TreeNode metadataNode) throws JsonProcessingException {
if (metadataNode == null) {
return new HashMap<>();
}

TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {};
return MAPPER.treeToValue(metadataNode, typeRef);
}

private static String transposeEvaluators(final String configuration) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.model;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Map;
import lombok.Getter;

/**
* The result of the parsing of a json string containing feature flag definitions.
*/
@Getter
@SuppressFBWarnings(
value = {"EI_EXPOSE_REP"},
justification = "Feature flag comes as a Json configuration, hence they must be exposed")
public class ParsingResult {
private final Map<String, FeatureFlag> flags;
private final Map<String, Object> globalFlagMetadata;

public ParsingResult(Map<String, FeatureFlag> flags, Map<String, Object> globalFlagMetadata) {
this.flags = flags;
this.globalFlagMetadata = globalFlagMetadata;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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.model.ParsingResult;
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;
Expand Down Expand Up @@ -35,6 +36,7 @@ public class FlagStore implements Storage {
private final AtomicBoolean shutdown = new AtomicBoolean(false);
private final BlockingQueue<StorageStateChange> stateBlockingQueue = new LinkedBlockingQueue<>(1);
private final Map<String, FeatureFlag> flags = new HashMap<>();
private final Map<String, Object> globalFlagMetadata = new HashMap<>();

private final Connector connector;
private final boolean throwIfInvalid;
Expand All @@ -49,6 +51,7 @@ public FlagStore(final Connector connector, final boolean throwIfInvalid) {
}

/** Initialize storage layer. */
@Override
public void init() throws Exception {
connector.init();
Thread streamer = new Thread(() -> {
Expand All @@ -68,6 +71,7 @@ public void init() throws Exception {
*
* @throws InterruptedException if stream can't be closed within deadline.
*/
@Override
public void shutdown() throws InterruptedException {
if (shutdown.getAndSet(true)) {
return;
Expand All @@ -76,17 +80,23 @@ public void shutdown() throws InterruptedException {
connector.shutdown();
}

/** Retrieve flag for the given key. */
public FeatureFlag getFlag(final String key) {
/** Retrieve flag for the given key and the global flag metadata. */
@Override
public StorageQueryResult getFlag(final String key) {
readLock.lock();
FeatureFlag flag;
Map<String, Object> metadata;
try {
return flags.get(key);
flag = flags.get(key);
metadata = new HashMap<>(globalFlagMetadata);
} finally {
readLock.unlock();
}
return new StorageQueryResult(flag, metadata);
}

/** Retrieve blocking queue to check storage status. */
@Override
public BlockingQueue<StorageStateChange> getStateQueue() {
return stateBlockingQueue;
}
Expand All @@ -100,14 +110,18 @@ private void streamerListener(final Connector connector) throws InterruptedExcep
case DATA:
try {
List<String> changedFlagsKeys;
Map<String, FeatureFlag> flagMap =
FlagParser.parseString(payload.getFlagData(), throwIfInvalid);
ParsingResult parsingResult = FlagParser.parseString(payload.getFlagData(), throwIfInvalid);
Map<String, FeatureFlag> flagMap = parsingResult.getFlags();
Map<String, Object> globalFlagMetadataMap = parsingResult.getGlobalFlagMetadata();

Structure metadata = parseSyncMetadata(payload.getMetadataResponse());
writeLock.lock();
try {
changedFlagsKeys = getChangedFlagsKeys(flagMap);
flags.clear();
flags.putAll(flagMap);
globalFlagMetadata.clear();
globalFlagMetadata.putAll(globalFlagMetadataMap);
} finally {
writeLock.unlock();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;

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

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

void shutdown() throws InterruptedException;

FeatureFlag getFlag(final String key);
StorageQueryResult getFlag(final String key);

BlockingQueue<StorageStateChange> getStateQueue();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;

import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Map;
import lombok.Getter;

/**
* To be returned by the storage when a flag is queried. Contains the flag (iff a flag associated with the given key
* exists, null otherwise) and global flag metadata
*/
@Getter
@SuppressFBWarnings(
value = {"EI_EXPOSE_REP"},
justification = "The storage provides access to both feature flags and global metadata")
public class StorageQueryResult {
private final FeatureFlag featureFlag;
private final Map<String, Object> globalFlagMetadata;
toddbaert marked this conversation as resolved.
Show resolved Hide resolved

public StorageQueryResult(FeatureFlag featureFlag, Map<String, Object> globalFlagMetadata) {
this.featureFlag = featureFlag;
this.globalFlagMetadata = globalFlagMetadata;
}
}
Loading
Loading