Skip to content

Conversation

@FarhanAnjum-opti
Copy link
Contributor

@FarhanAnjum-opti FarhanAnjum-opti commented Sep 24, 2025

Summary

Decision Service methods to handle CMAB

Test plan

Added unit tests

Issues

FSSDK-11170

…ations over CMAB service decisions in DecisionService
Copy link
Contributor

@muzahidul-opti muzahidul-opti left a comment

Choose a reason for hiding this comment

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

Changes look good to me. Added few comments.

Copy link
Contributor

@muzahidul-opti muzahidul-opti left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

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

A few changes in API definitions. Can you refactor them first?
I need more time to review the cmab logic.

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.

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

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

A few more suggestions at the top level.

Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
FarhanAnjum-opti and others added 7 commits October 23, 2025 20:54
Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
…ckage-private and add copyright notice to DecisionPath
…and OptimizelyUserContext with corresponding tests
…and enhance builder methods for cache configuration
Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

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

I see it's now more aligned with android-sdk. I added more comments. Let's discuss.

}
} else {
// Standard bucketing for non-CMAB experiments
decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig);
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's share this bucket() for cmab as well - they share group bucketing and variation allocations.
The only exception is that when it returns a valid variation from bucket, we should ignore it and get the response from cmab server. We can remove bucketForCmab.

Copy link
Contributor

Choose a reason for hiding this comment

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

A flow like this?

decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig);
if is_cmab && decisionVariaion != null {
   // group-allocation and traffic-allocation checking passed for cmab
   // we need server decision overruling local bucketing for cmab
   variation = getDecisionForCmabExperiment()
   ...
} else {
   reasons.merge(decisionVariation.getReasons());
   variation = decisionVariation.getResult();
}


if (decision != null) {
decisions.add(new DecisionResponse(decision, reasons));
decisions.add(new DecisionResponse(decision, reasons, error, decision.cmabUUID));
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 have any case we need pass cmabUUID separately though it's already in decision?

Comment on lines +943 to +958
// Check if user is in CMAB traffic allocation
DecisionResponse<Variation> bucketResponse = bucketer.bucket(experiment, bucketingId, projectConfig, true);
// DecisionResponse<String> bucketResponse = bucketer.bucketForCmab(experiment, bucketingId, projectConfig);
reasons.merge(bucketResponse.getReasons());

Variation bucketedVariation = bucketResponse.getResult();
String bucketedEntityId = bucketedVariation != null ? bucketedVariation.getId() : null;

if (bucketedEntityId == null) {
String message = String.format("User \"%s\" not in CMAB experiment \"%s\" due to traffic allocation.",
userContext.getUserId(), experiment.getKey());
logger.info(message);
reasons.addInfo(message);

return new DecisionResponse<>(null, reasons);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We can remove these block when sharing "bucket" with our experiment types in the caller.

private static final String INVALID_CMAB_FETCH_RESPONSE = "Invalid CMAB fetch response";
private static final Pattern VARIATION_ID_PATTERN = Pattern.compile("\"variation_id\"\\s*:\\s*\"?([^\"\\s,}]+)\"?");

private static final String CMAB_PREDICTION_ENDPOINT = "https://prediction.cmab.optimizely.com/predict/%s";
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we move this to CmabClientHelper too? So android-sdk can use it from CmabClientHelper.

Comment on lines +213 to +245
/**
* Convenience method for setting the CMAB cache size.
* {@link DefaultCmabService.Builder#withCmabCacheSize(int)}
*
* @param cacheSize The maximum number of CMAB cache entries
*/
public static void setCmabCacheSize(int cacheSize) {
if (cacheSize <= 0) {
logger.warn("CMAB cache size cannot be <= 0. Reverting to default configuration.");
return;
}
PropertyUtils.set("optimizely.cmab.cache.size", Integer.toString(cacheSize));
}

/**
* Convenience method for setting the CMAB cache timeout.
* {@link DefaultCmabService.Builder#withCmabCacheTimeoutInSecs(int)}
*
* @param timeoutInSecs The timeout in seconds before CMAB cache entries expire
*/
public static void setCmabCacheTimeoutInSecs(int timeoutInSecs) {
if (timeoutInSecs <= 0) {
logger.warn("CMAB cache timeout cannot be <= 0. Reverting to default configuration.");
return;
}
PropertyUtils.set("optimizely.cmab.cache.timeout", Integer.toString(timeoutInSecs));
}

/**
* Convenience method for setting a custom CMAB cache implementation.
* {@link DefaultCmabService.Builder#withCustomCache(Cache)}
*
* @param cache The custom cache implementation
Copy link
Contributor

Choose a reason for hiding this comment

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

We may drop these. We can build CmabService and inject it.

Comment on lines +438 to +445
String cacheTimeoutStr = PropertyUtils.get("optimizely.cmab.cache.timeout");
if (cacheTimeoutStr != null) {
try {
cmabBuilder.withCmabCacheTimeoutInSecs(Integer.parseInt(cacheTimeoutStr));
} catch (NumberFormatException e) {
logger.warn("Invalid CMAB cache timeout property value: {}", cacheTimeoutStr);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let the user build with cache config (instead of setProp) and inject cmabService.
https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java#odpeventmanager

Copy link
Contributor

Choose a reason for hiding this comment

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

We can follow the same pattern as ODPManager for consistency.

public Builder withLogger(Logger logger) {
this.logger = logger;
return this;
}
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 need to inject logger here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants