Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
26e6393
update: add CmabService to Optimizely class and builder
FarhanAnjum-opti Sep 19, 2025
ad63201
update: integrate CMAB service into OptimizelyFactory
FarhanAnjum-opti Sep 19, 2025
fbed362
update: change CmabService field to non-nullable in Optimizely class
FarhanAnjum-opti Sep 19, 2025
53d754a
update: add CmabService to DecisionService and its tests
FarhanAnjum-opti Sep 19, 2025
9905026
update: implement CMAB traffic allocation in Bucketer and DecisionSer…
FarhanAnjum-opti Sep 23, 2025
78f45bf
update: enhance DecisionService, FeatureDecision, and DecisionRespons…
FarhanAnjum-opti Sep 24, 2025
9757d49
update: enhance DecisionService and DecisionMessage to handle errors …
FarhanAnjum-opti Sep 24, 2025
ecf9199
update: add validConfigJsonCMAB method to DatafileProjectConfigTestUt…
FarhanAnjum-opti Sep 24, 2025
5e0808f
update: add tests to verify precedence of whitelisted and forced vari…
FarhanAnjum-opti Sep 24, 2025
36d2b4c
update: add test to verify error handling in getVariation for CMAB se…
FarhanAnjum-opti Sep 24, 2025
5796cb7
update: modify DecisionResponse to include additional error handling …
FarhanAnjum-opti Sep 24, 2025
d8b0134
update: fix error handling assertion in DecisionServiceTest to correc…
FarhanAnjum-opti Sep 24, 2025
b2f270f
update: add tests for CMAB experiment variations in DecisionService
FarhanAnjum-opti Sep 24, 2025
a4c3f1c
update: implement decision-making methods to skip CMAB logic in Optim…
FarhanAnjum-opti Sep 24, 2025
e4fe788
update: add methods to OptimizelyUserContext for decision-making with…
FarhanAnjum-opti Sep 24, 2025
e75693d
update: add asynchronous decision-making methods in OptimizelyUserCon…
FarhanAnjum-opti Sep 24, 2025
af210d8
update: add decision-making methods without CMAB logic in OptimizelyU…
FarhanAnjum-opti Sep 24, 2025
42053e4
update: remove unused parameter 'useCmab' from DecisionService method…
FarhanAnjum-opti Sep 24, 2025
9a12d72
update: rename methods to use 'Sync' suffix for clarity in decision-m…
FarhanAnjum-opti Sep 25, 2025
416bcbd
update: return cmab error decision whenever found
FarhanAnjum-opti Sep 30, 2025
64f378f
update: enhance error handling by specifying CMAB error messages in d…
FarhanAnjum-opti Oct 1, 2025
8539166
update: improve error handling by checking for null values in experim…
FarhanAnjum-opti Oct 1, 2025
3cee65c
update: fix CMAB error handling by providing a valid Experiment in Fe…
FarhanAnjum-opti Oct 1, 2025
47c65b5
update: add Javadoc comments for async decision methods and config cr…
FarhanAnjum-opti Oct 1, 2025
fe75a85
update: refactor build to use cmabClient instead of default service
FarhanAnjum-opti Oct 3, 2025
b0d5090
update: refactor DefaultCmabClient to utilize CmabClientHelper
FarhanAnjum-opti Oct 3, 2025
6db2e88
update: refactor AsyncDecisionsFetcher to AsyncDecisionFetcher and en…
FarhanAnjum-opti Oct 3, 2025
6fc6446
update: add missing copyright notice and license information to CmabC…
FarhanAnjum-opti Oct 3, 2025
a80c0d3
update: enhance CMAB handling in bucketing and decision services, add…
FarhanAnjum-opti Oct 15, 2025
a9ae805
update: add backward compatibility support for Android sync and async…
FarhanAnjum-opti Oct 15, 2025
f25f824
update: add empty list parameter to decision methods in OptimizelyUse…
FarhanAnjum-opti Oct 15, 2025
7363a2f
update: replace null with empty list parameter in async decision meth…
FarhanAnjum-opti Oct 15, 2025
1c52366
update: add useCmab parameter to decideForKeys methods for enhanced d…
FarhanAnjum-opti Oct 15, 2025
b2dcf9e
Update core-api/src/main/java/com/optimizely/ab/Optimizely.java
FarhanAnjum-opti Oct 17, 2025
89771bc
update: refactor decision-making logic to use DecisionPath enum for c…
FarhanAnjum-opti Oct 23, 2025
73d5673
Update core-api/src/main/java/com/optimizely/ab/Optimizely.java
FarhanAnjum-opti Oct 23, 2025
690379c
Update core-api/src/main/java/com/optimizely/ab/Optimizely.java
FarhanAnjum-opti Oct 23, 2025
9c8dd8f
update: modify OptimizelyUserContext to change optimizely field to pa…
FarhanAnjum-opti Oct 27, 2025
a17becd
update: implement asynchronous decision-making methods in Optimizely …
FarhanAnjum-opti Oct 27, 2025
ba575ce
update: refactor DefaultCmabService to remove CmabServiceOptions depe…
FarhanAnjum-opti Oct 27, 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
134 changes: 128 additions & 6 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -1395,7 +1408,8 @@ Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext use
private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options,
boolean ignoreDefaultOptions) {
boolean ignoreDefaultOptions,
boolean useCmab) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();

ProjectConfig projectConfig = getProjectConfig();
Expand Down Expand Up @@ -1440,11 +1454,25 @@ private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserCon
}

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

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));
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

@FarhanAnjum-opti FarhanAnjum-opti Oct 15, 2025

Choose a reason for hiding this comment

The 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());
}
Expand All @@ -1465,6 +1493,13 @@ private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserCon
return decisionMap;
}

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

Map<String, OptimizelyDecision> decideAll(@Nonnull OptimizelyUserContext user,
@Nonnull List<OptimizelyDecideOption> options) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
Expand All @@ -1482,6 +1517,78 @@ 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.
* This will be called by mobile apps which will use non-blocking legacy ab-tests only (for backward compatibility with android-sdk)
*
* @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.
* This will be called by mobile apps which will use non-blocking legacy ab-tests only (for backward compatibility with android-sdk)
*
* @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.
* This will be called by mobile apps which will make synchronous decisions only (for backward compatibility with android-sdk)
*
* @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) {
return decideForKeys(user, keys, options, ignoreDefaultOptions, false);
}


private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption> options) {
List<OptimizelyDecideOption> copiedOptions = new ArrayList(defaultDecideOptions);
if (options != null) {
Expand Down Expand Up @@ -1731,6 +1838,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();
Expand Down Expand Up @@ -1842,6 +1950,16 @@ public Builder withODPManager(ODPManager odpManager) {
return this;
}

public Builder withCmabClient(CmabClient cmabClient) {
Copy link
Contributor

@jaeopt jaeopt Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FarhanAnjum-opti It looks like we need to build withCmabService(), so others like android-sdk can inject full-featured CmabService with client + cacheSize + cacheTTL.
We can provide Builder pattern to CmabService like OdpManager -

CmabServiceBuilder() .withCmabCacheSize() .withCmabCachTimeoutInSecs() .withClient() .build()

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);
Comment on lines 1992 to 1994
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
int DEFAULT_CMAB_CACHE_TIMEOUT_IN_SECS = 30 * 60;
``
I believe DefaultLRUCache accepts timeout in secs not in millisecs. Can we add a test case for DefaultLRUCache to validate this error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand cmabCacheSize and cmabCacheTimeout not configurable for now?

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;
Expand Down Expand Up @@ -1872,8 +1990,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()) {
Expand Down Expand Up @@ -1916,7 +2038,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);
}
}
}
104 changes: 95 additions & 9 deletions core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,26 @@
*/
package com.optimizely.ab;

import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.odp.ODPManager;
import com.optimizely.ab.odp.ODPSegmentCallback;
import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelydecision.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.optimizely.ab.odp.ODPSegmentCallback;
import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelydecision.AsyncDecisionFetcher;
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionCallback;
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionsCallback;

public class OptimizelyUserContext {
// OptimizelyForcedDecisionsKey mapped to variationKeys
Expand Down Expand Up @@ -197,6 +205,84 @@ public Map<String, OptimizelyDecision> decideAll() {
return decideAll(Collections.emptyList());
}

/**
* Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context,
* which contains all data required to deliver the flag. This method skips CMAB logic.
* backward compatibility support for android sync decisions
* @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.
*/
public OptimizelyDecision decideSync(@Nonnull String key,
@Nonnull List<OptimizelyDecideOption> options) {
return optimizely.decideSync(copy(), key, options);
}

/**
* Returns a key-map of decision results ({@link OptimizelyDecision}) for multiple flag keys and a user context.
* This method skips CMAB logic.
* backward compatibility support for android sync decisions
* @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.
*/
public Map<String, OptimizelyDecision> decideForKeysSync(@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options) {
return optimizely.decideForKeysSync(copy(), keys, options);
}

/**
* Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys.
* This method skips CMAB logic.
* backward compatibility support for android sync decisions
* @param options A list of options for decision-making.
* @return All decision results mapped by flag keys.
*/
public Map<String, OptimizelyDecision> decideAllSync(@Nonnull List<OptimizelyDecideOption> options) {
return optimizely.decideAllSync(copy(), options);
}

/**
* Returns a decision result asynchronously for a given flag key and a user context.
* support for android async decisions
* @param key A flag key for which a decision will be made.
* @param callback A callback to invoke when the decision is available.
* @param options A list of options for decision-making.
*/
public void decideAsync(@Nonnull String key,
@Nonnull OptimizelyDecisionCallback callback,
@Nonnull List<OptimizelyDecideOption> options) {
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(this, key, options, callback);
fetcher.start();
}


/**
* Returns decision results asynchronously for multiple flag keys.
* support for android async decisions
* @param keys A list of flag keys for which decisions will be made.
* @param callback A callback to invoke when decisions are available.
* @param options A list of options for decision-making.
*/
public void decideForKeysAsync(@Nonnull List<String> keys,
@Nonnull OptimizelyDecisionsCallback callback,
@Nonnull List<OptimizelyDecideOption> options) {
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(this, keys, options, callback);
fetcher.start();
}

/**
* Returns decision results asynchronously for all active flag keys.
* support for android async decisions
* @param callback A callback to invoke when decisions are available.
* @param options A list of options for decision-making.
*/
public void decideAllAsync(@Nonnull OptimizelyDecisionsCallback callback,
@Nonnull List<OptimizelyDecideOption> options) {
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(this, options, callback);
fetcher.start();
}

/**
* Track an event.
*
Expand Down
Loading
Loading