- 
                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?
[FSSDK-11170] update: decision service methods for cmab #583
Conversation
…e to support CMAB UUID handling
…and include CMAB UUIDs in responses
…ils for CMAB configuration
…ations over CMAB service decisions in DecisionService
…tly verify error state
…izely and DecisionService
…text and related fetcher classes
There was a problem hiding this 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.
        
          
                core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionResponse.java
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/optimizelydecision/AsyncDecisionsFetcher.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
      …ecision responses
…ent key retrieval
…eation in CMAB client
…hance decision handling
        
          
                core-api/src/main/java/com/optimizely/ab/cmab/client/CmabClientHelper.java
          
            Show resolved
            Hide resolved
        
      There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this 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)); | 
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
        
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
      … backward compatibility for mobile apps
… decisions in OptimizelyUserContext
…rContextTest for consistency
…od for consistency
There was a problem hiding this 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.
        
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
      Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
…larity and maintainability
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
…ndency and adjust related tests
…and enhance builder methods for cache configuration
There was a problem hiding this 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); | 
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)); | 
There was a problem hiding this comment.
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?
| // 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); | ||
| } | 
There was a problem hiding this comment.
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"; | 
There was a problem hiding this comment.
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.
| /** | ||
| * 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 | 
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| } | 
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; | ||
| } | 
There was a problem hiding this comment.
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?
Summary
Decision Service methods to handle CMAB
Test plan
Added unit tests
Issues
FSSDK-11170