Skip to content

Commit

Permalink
[FSSDK-10762] Implement UPS request batching for decideForKeys (#549)
Browse files Browse the repository at this point in the history
  • Loading branch information
raju-opti authored Oct 22, 2024
1 parent e813bfe commit fe50318
Show file tree
Hide file tree
Showing 5 changed files with 458 additions and 141 deletions.
128 changes: 87 additions & 41 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2016-2023, Optimizely, Inc. and contributors *
* Copyright 2016-2024, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -42,8 +42,10 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

import java.io.Closeable;
import java.util.*;
import java.util.stream.Collectors;

import static com.optimizely.ab.internal.SafetyUtils.tryClose;

Expand Down Expand Up @@ -1194,55 +1196,39 @@ private OptimizelyUserContext createUserContextCopy(@Nonnull String userId, @Non
OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
@Nonnull String key,
@Nonnull List<OptimizelyDecideOption> options) {

ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
}

FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key);
if (flag == null) {
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key));
}

String userId = user.getUserId();
Map<String, Object> attributes = user.getAttributes();
Boolean decisionEventDispatched = false;
List<OptimizelyDecideOption> allOptions = getAllOptions(options);
DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
allOptions.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY);

Map<String, ?> copiedAttributes = new HashMap<>(attributes);
FeatureDecision flagDecision;

// Check Forced Decision
OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flag.getKey(), null);
DecisionResponse<Variation> forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user);
decisionReasons.merge(forcedDecisionVariation.getReasons());
if (forcedDecisionVariation.getResult() != null) {
flagDecision = new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST);
} else {
// Regular decision
DecisionResponse<FeatureDecision> decisionVariation = decisionService.getVariationForFeature(
flag,
user,
projectConfig,
allOptions);
flagDecision = decisionVariation.getResult();
decisionReasons.merge(decisionVariation.getReasons());
}
return decideForKeys(user, Arrays.asList(key), allOptions, true).get(key);
}

private OptimizelyDecision createOptimizelyDecision(
OptimizelyUserContext user,
String flagKey,
FeatureDecision flagDecision,
DecisionReasons decisionReasons,
List<OptimizelyDecideOption> allOptions,
ProjectConfig projectConfig
) {
String userId = user.getUserId();

Boolean flagEnabled = false;
if (flagDecision.variation != null) {
if (flagDecision.variation.getFeatureEnabled()) {
flagEnabled = true;
}
}
logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled);
logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", flagKey, userId, flagEnabled);

Map<String, Object> variableMap = new HashMap<>();
if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) {
DecisionResponse<Map<String, Object>> decisionVariables = getDecisionVariableMap(
flag,
projectConfig.getFeatureKeyMapping().get(flagKey),
flagDecision.variation,
flagEnabled);
variableMap = decisionVariables.getResult();
Expand All @@ -1261,22 +1247,28 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
// add to event metadata as well (currently set to experimentKey)
String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null;


Boolean decisionEventDispatched = false;

Map<String, Object> attributes = user.getAttributes();
Map<String, ?> copiedAttributes = new HashMap<>(attributes);

if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) {
decisionEventDispatched = sendImpression(
projectConfig,
flagDecision.experiment,
userId,
copiedAttributes,
flagDecision.variation,
key,
flagKey,
decisionSource.toString(),
flagEnabled);
}

DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder()
.withUserId(userId)
.withAttributes(copiedAttributes)
.withFlagKey(key)
.withFlagKey(flagKey)
.withEnabled(flagEnabled)
.withVariables(variableMap)
.withVariationKey(variationKey)
Expand All @@ -1291,30 +1283,84 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
flagEnabled,
optimizelyJSON,
ruleKey,
key,
flagKey,
user,
reasonsToReport);
}

Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options) {
return decideForKeys(user, keys, options, false);
}

private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options) {
@Nonnull List<OptimizelyDecideOption> options,
boolean ignoreDefaultOptions) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();

ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
logger.error("Optimizely instance is not valid, failing isFeatureEnabled call.");
logger.error("Optimizely instance is not valid, failing decideForKeys call.");
return decisionMap;
}

if (keys.isEmpty()) return decisionMap;

List<OptimizelyDecideOption> allOptions = getAllOptions(options);
List<OptimizelyDecideOption> allOptions = ignoreDefaultOptions ? options: getAllOptions(options);

Map<String, FeatureDecision> flagDecisions = new HashMap<>();
Map<String, DecisionReasons> decisionReasonsMap = new HashMap<>();

List<FeatureFlag> flagsWithoutForcedDecision = new ArrayList<>();

List<String> validKeys = new ArrayList<>();

for (String key : keys) {
OptimizelyDecision decision = decide(user, key, options);
if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.getEnabled()) {
decisionMap.put(key, decision);
FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key);
if (flag == null) {
decisionMap.put(key, OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key)));
continue;
}

validKeys.add(key);

DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
decisionReasonsMap.put(key, decisionReasons);

OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(key, null);
DecisionResponse<Variation> forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user);
decisionReasons.merge(forcedDecisionVariation.getReasons());

if (forcedDecisionVariation.getResult() != null) {
flagDecisions.put(key,
new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST));
} else {
flagsWithoutForcedDecision.add(flag);
}
}

List<DecisionResponse<FeatureDecision>> decisionList =
decisionService.getVariationsForFeatureList(flagsWithoutForcedDecision, user, projectConfig, allOptions);

for (int i = 0; i < flagsWithoutForcedDecision.size(); i++) {
DecisionResponse<FeatureDecision> decision = decisionList.get(i);
String flagKey = flagsWithoutForcedDecision.get(i).getKey();
flagDecisions.put(flagKey, decision.getResult());
decisionReasonsMap.get(flagKey).merge(decision.getReasons());
}

for (String key: validKeys) {
FeatureDecision flagDecision = flagDecisions.get(key);
DecisionReasons decisionReasons = decisionReasonsMap.get((key));

OptimizelyDecision optimizelyDecision = createOptimizelyDecision(
user, key, flagDecision, decisionReasons, allOptions, projectConfig
);

if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || optimizelyDecision.getEnabled()) {
decisionMap.put(key, optimizelyDecision);
}
}

Expand Down
Loading

0 comments on commit fe50318

Please sign in to comment.