-
Notifications
You must be signed in to change notification settings - Fork 32
[FSSDK-11170] update: decision service methods for cmab #583
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
base: master
Are you sure you want to change the base?
Changes from 28 commits
26e6393
ad63201
fbed362
53d754a
9905026
78f45bf
9757d49
ecf9199
5e0808f
36d2b4c
5796cb7
d8b0134
b2f270f
a4c3f1c
e4fe788
e75693d
af210d8
42053e4
9a12d72
416bcbd
64f378f
8539166
3cee65c
47c65b5
fe75a85
b0d5090
6db2e88
6fc6446
a80c0d3
a9ae805
f25f824
7363a2f
1c52366
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,10 @@ | |
import com.optimizely.ab.bucketing.DecisionService; | ||
import com.optimizely.ab.bucketing.FeatureDecision; | ||
import com.optimizely.ab.bucketing.UserProfileService; | ||
import com.optimizely.ab.cmab.service.CmabCacheValue; | ||
import com.optimizely.ab.cmab.service.CmabService; | ||
import com.optimizely.ab.cmab.service.CmabServiceOptions; | ||
import com.optimizely.ab.cmab.service.DefaultCmabService; | ||
import com.optimizely.ab.config.AtomicProjectConfigManager; | ||
import com.optimizely.ab.config.DatafileProjectConfig; | ||
import com.optimizely.ab.config.EventType; | ||
|
@@ -45,6 +49,7 @@ | |
import com.optimizely.ab.event.internal.UserEvent; | ||
import com.optimizely.ab.event.internal.UserEventFactory; | ||
import com.optimizely.ab.event.internal.payload.EventBatch; | ||
import com.optimizely.ab.internal.DefaultLRUCache; | ||
import com.optimizely.ab.internal.NotificationRegistry; | ||
import com.optimizely.ab.notification.ActivateNotification; | ||
import com.optimizely.ab.notification.DecisionNotification; | ||
|
@@ -69,12 +74,14 @@ | |
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; | ||
import com.optimizely.ab.optimizelydecision.OptimizelyDecision; | ||
import com.optimizely.ab.optimizelyjson.OptimizelyJSON; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.annotation.Nonnull; | ||
import javax.annotation.Nullable; | ||
import javax.annotation.concurrent.ThreadSafe; | ||
|
||
import java.io.Closeable; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
|
@@ -84,6 +91,7 @@ | |
import java.util.Map; | ||
import java.util.concurrent.locks.ReentrantLock; | ||
|
||
import com.optimizely.ab.cmab.client.CmabClient; | ||
import static com.optimizely.ab.internal.SafetyUtils.tryClose; | ||
|
||
/** | ||
|
@@ -141,8 +149,11 @@ public class Optimizely implements AutoCloseable { | |
@Nullable | ||
private final ODPManager odpManager; | ||
|
||
private final CmabService cmabService; | ||
|
||
private final ReentrantLock lock = new ReentrantLock(); | ||
|
||
|
||
private Optimizely(@Nonnull EventHandler eventHandler, | ||
@Nonnull EventProcessor eventProcessor, | ||
@Nonnull ErrorHandler errorHandler, | ||
|
@@ -152,8 +163,9 @@ private Optimizely(@Nonnull EventHandler eventHandler, | |
@Nullable OptimizelyConfigManager optimizelyConfigManager, | ||
@Nonnull NotificationCenter notificationCenter, | ||
@Nonnull List<OptimizelyDecideOption> defaultDecideOptions, | ||
@Nullable ODPManager odpManager | ||
) { | ||
@Nullable ODPManager odpManager, | ||
@Nonnull CmabService cmabService | ||
) { | ||
this.eventHandler = eventHandler; | ||
this.eventProcessor = eventProcessor; | ||
this.errorHandler = errorHandler; | ||
|
@@ -164,6 +176,7 @@ private Optimizely(@Nonnull EventHandler eventHandler, | |
this.notificationCenter = notificationCenter; | ||
this.defaultDecideOptions = defaultDecideOptions; | ||
this.odpManager = odpManager; | ||
this.cmabService = cmabService; | ||
|
||
if (odpManager != null) { | ||
odpManager.getEventManager().start(); | ||
|
@@ -1444,7 +1457,21 @@ private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserCon | |
|
||
for (int i = 0; i < flagsWithoutForcedDecision.size(); i++) { | ||
DecisionResponse<FeatureDecision> decision = decisionList.get(i); | ||
boolean error = decision.isError(); | ||
String experimentKey = null; | ||
if (decision.getResult() != null && decision.getResult().experiment != null) { | ||
experimentKey = decision.getResult().experiment.getKey(); | ||
} | ||
String flagKey = flagsWithoutForcedDecision.get(i).getKey(); | ||
|
||
if (error) { | ||
OptimizelyDecision optimizelyDecision = OptimizelyDecision.newErrorDecision(flagKey, user, DecisionMessage.CMAB_ERROR.reason(experimentKey)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we always report CMAB error for any decision errors? Is this safe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I understand, we get error from decision service only when cmab fails. So this error flag is only true for cmab errors. @raju-opti can verify. |
||
decisionMap.put(flagKey, optimizelyDecision); | ||
if (validKeys.contains(flagKey)) { | ||
validKeys.remove(flagKey); | ||
} | ||
} | ||
|
||
flagDecisions.put(flagKey, decision.getResult()); | ||
decisionReasonsMap.get(flagKey).merge(decision.getReasons()); | ||
} | ||
|
@@ -1482,6 +1509,141 @@ Map<String, OptimizelyDecision> decideAll(@Nonnull OptimizelyUserContext user, | |
return decideForKeys(user, allFlagKeys, options); | ||
} | ||
|
||
/** | ||
* Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, | ||
* skipping CMAB logic and using only traditional A/B testing. | ||
FarhanAnjum-opti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @param user An OptimizelyUserContext associated with this OptimizelyClient. | ||
* @param key A flag key for which a decision will be made. | ||
* @param options A list of options for decision-making. | ||
* @return A decision result using traditional A/B testing logic only. | ||
*/ | ||
OptimizelyDecision decideSync(@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()); | ||
} | ||
|
||
List<OptimizelyDecideOption> allOptions = getAllOptions(options); | ||
allOptions.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY); | ||
|
||
return decideForKeysSync(user, Arrays.asList(key), allOptions, true).get(key); | ||
} | ||
|
||
/** | ||
* Returns decision results for multiple flag keys, skipping CMAB logic and using only traditional A/B testing. | ||
* | ||
* @param user An OptimizelyUserContext associated with this OptimizelyClient. | ||
* @param keys A list of flag keys for which decisions will be made. | ||
* @param options A list of options for decision-making. | ||
* @return All decision results mapped by flag keys, using traditional A/B testing logic only. | ||
*/ | ||
Map<String, OptimizelyDecision> decideForKeysSync(@Nonnull OptimizelyUserContext user, | ||
@Nonnull List<String> keys, | ||
@Nonnull List<OptimizelyDecideOption> options) { | ||
return decideForKeysSync(user, keys, options, false); | ||
} | ||
|
||
/** | ||
* Returns decision results for all active flag keys, skipping CMAB logic and using only traditional A/B testing. | ||
* | ||
* @param user An OptimizelyUserContext associated with this OptimizelyClient. | ||
* @param options A list of options for decision-making. | ||
* @return All decision results mapped by flag keys, using traditional A/B testing logic only. | ||
*/ | ||
Map<String, OptimizelyDecision> decideAllSync(@Nonnull OptimizelyUserContext user, | ||
@Nonnull List<OptimizelyDecideOption> options) { | ||
Map<String, OptimizelyDecision> decisionMap = new HashMap<>(); | ||
|
||
ProjectConfig projectConfig = getProjectConfig(); | ||
if (projectConfig == null) { | ||
logger.error("Optimizely instance is not valid, failing decideAllSync call."); | ||
return decisionMap; | ||
} | ||
|
||
List<FeatureFlag> allFlags = projectConfig.getFeatureFlags(); | ||
List<String> allFlagKeys = new ArrayList<>(); | ||
for (int i = 0; i < allFlags.size(); i++) allFlagKeys.add(allFlags.get(i).getKey()); | ||
|
||
return decideForKeysSync(user, allFlagKeys, options); | ||
} | ||
|
||
private Map<String, OptimizelyDecision> decideForKeysSync(@Nonnull OptimizelyUserContext user, | ||
@Nonnull List<String> keys, | ||
@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 decideForKeysSync call."); | ||
return decisionMap; | ||
} | ||
|
||
if (keys.isEmpty()) return decisionMap; | ||
|
||
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) { | ||
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); | ||
} | ||
} | ||
|
||
// Use DecisionService method that skips CMAB logic | ||
List<DecisionResponse<FeatureDecision>> decisionList = | ||
decisionService.getVariationsForFeatureList(flagsWithoutForcedDecision, user, projectConfig, allOptions, false); | ||
|
||
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); | ||
} | ||
} | ||
|
||
return decisionMap; | ||
} | ||
|
||
|
||
private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption> options) { | ||
List<OptimizelyDecideOption> copiedOptions = new ArrayList(defaultDecideOptions); | ||
if (options != null) { | ||
|
@@ -1731,6 +1893,7 @@ public static class Builder { | |
private NotificationCenter notificationCenter; | ||
private List<OptimizelyDecideOption> defaultDecideOptions; | ||
private ODPManager odpManager; | ||
private CmabService cmabService; | ||
|
||
// For backwards compatibility | ||
private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager(); | ||
|
@@ -1842,6 +2005,16 @@ public Builder withODPManager(ODPManager odpManager) { | |
return this; | ||
} | ||
|
||
public Builder withCmabClient(CmabClient cmabClient) { | ||
int DEFAULT_MAX_SIZE = 1000; | ||
int DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 * 1000; | ||
DefaultLRUCache<CmabCacheValue> cmabCache = new DefaultLRUCache<>(DEFAULT_MAX_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT); | ||
CmabServiceOptions cmabServiceOptions = new CmabServiceOptions(logger, cmabCache, cmabClient); | ||
DefaultCmabService defaultCmabService = new DefaultCmabService(cmabServiceOptions); | ||
this.cmabService = defaultCmabService; | ||
return this; | ||
} | ||
|
||
// Helper functions for making testing easier | ||
protected Builder withBucketing(Bucketer bucketer) { | ||
this.bucketer = bucketer; | ||
|
@@ -1872,8 +2045,12 @@ public Optimizely build() { | |
bucketer = new Bucketer(); | ||
} | ||
|
||
if (cmabService == null) { | ||
logger.warn("CMAB service is not initiated. CMAB functionality will not be available."); | ||
} | ||
|
||
if (decisionService == null) { | ||
decisionService = new DecisionService(bucketer, errorHandler, userProfileService); | ||
decisionService = new DecisionService(bucketer, errorHandler, userProfileService, cmabService); | ||
} | ||
|
||
if (projectConfig == null && datafile != null && !datafile.isEmpty()) { | ||
|
@@ -1916,7 +2093,7 @@ public Optimizely build() { | |
defaultDecideOptions = Collections.emptyList(); | ||
} | ||
|
||
return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions, odpManager); | ||
return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions, odpManager, cmabService); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.