Skip to content

Commit e459321

Browse files
update: refactor bucketing logic to remove CMAB handling from DecisionService and adjust tests accordingly
1 parent 93cdaba commit e459321

File tree

3 files changed

+17
-106
lines changed

3 files changed

+17
-106
lines changed

core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -128,49 +128,6 @@ private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore ex
128128
return new DecisionResponse(null, reasons);
129129
}
130130

131-
/**
132-
* Determines CMAB traffic allocation for a user based on hashed value from murmurhash3.
133-
* This method handles bucketing users into CMAB (Contextual Multi-Armed Bandit) experiments.
134-
*/
135-
@Nonnull
136-
private DecisionResponse<String> bucketToEntityForCmab(@Nonnull Experiment experiment,
137-
@Nonnull String bucketingId) {
138-
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
139-
140-
// "salt" the bucket id using the experiment id
141-
String experimentId = experiment.getId();
142-
String experimentKey = experiment.getKey();
143-
String combinedBucketId = bucketingId + experimentId;
144-
145-
// Handle CMAB traffic allocation
146-
TrafficAllocation cmabTrafficAllocation = new TrafficAllocation("$", experiment.getCmab().getTrafficAllocation());
147-
List<TrafficAllocation> trafficAllocations = java.util.Collections.singletonList(cmabTrafficAllocation);
148-
149-
String cmabMessage = reasons.addInfo("Using CMAB traffic allocation for experiment \"%s\".", experimentKey);
150-
logger.debug(cmabMessage);
151-
152-
int hashCode = MurmurHash3.murmurhash3_x86_32(combinedBucketId, 0, combinedBucketId.length(), MURMUR_HASH_SEED);
153-
int bucketValue = generateBucketValue(hashCode);
154-
logger.debug("Assigned bucket {} to user with bucketingId \"{}\" when bucketing to a variation.", bucketValue, bucketingId);
155-
156-
String bucketedEntityId = bucketToEntity(bucketValue, trafficAllocations);
157-
if (bucketedEntityId != null) {
158-
if ("$".equals(bucketedEntityId)) {
159-
String message = reasons.addInfo("User with bucketingId \"%s\" is bucketed into CMAB for experiment \"%s\".", bucketingId, experimentKey);
160-
logger.info(message);
161-
} else {
162-
// This shouldn't happen in CMAB since we only have "$" entity, but handle gracefully
163-
String message = reasons.addInfo("User with bucketingId \"%s\" is bucketed into entity \"%s\" for experiment \"%s\".", bucketingId, bucketedEntityId, experimentKey);
164-
logger.info(message);
165-
}
166-
} else {
167-
String message = reasons.addInfo("User with bucketingId \"%s\" is not bucketed into CMAB for experiment \"%s\".", bucketingId, experimentKey);
168-
logger.info(message);
169-
}
170-
171-
return new DecisionResponse<>(bucketedEntityId, reasons);
172-
}
173-
174131
/**
175132
* Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3.
176133
*
@@ -183,24 +140,6 @@ private DecisionResponse<String> bucketToEntityForCmab(@Nonnull Experiment exper
183140
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
184141
@Nonnull String bucketingId,
185142
@Nonnull ProjectConfig projectConfig) {
186-
187-
return bucket(experiment, bucketingId, projectConfig, false);
188-
}
189-
190-
/**
191-
* Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3.
192-
*
193-
* @param experiment The Experiment in which the user is to be bucketed.
194-
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
195-
* @param projectConfig The current projectConfig
196-
* @param useCmab boolean flag to decide whether to handle cmab experiments.
197-
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
198-
*/
199-
@Nonnull
200-
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
201-
@Nonnull String bucketingId,
202-
@Nonnull ProjectConfig projectConfig,
203-
@Nonnull boolean useCmab) {
204143
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
205144

206145
// ---------- Bucket User ----------
@@ -233,26 +172,9 @@ public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
233172
}
234173
}
235174

236-
if (useCmab){
237-
if (experiment instanceof Experiment) {
238-
DecisionResponse<String> decisionResponse = bucketToEntityForCmab((Experiment) experiment, bucketingId);
239-
reasons.merge(decisionResponse.getReasons());
240-
String entityId = decisionResponse.getResult();
241-
if (entityId==null){
242-
return new DecisionResponse<>(null, reasons);
243-
}
244-
Variation variation = new Variation(entityId, entityId); //return dummy variation for cmab
245-
return new DecisionResponse<>(variation, reasons);
246-
} else {
247-
String message = reasons.addInfo("ExperimentCore instance is not of type Experiment, cannot perform CMAB bucketing.");
248-
logger.info(message);
249-
return new DecisionResponse<>(null, reasons);
250-
}
251-
} else {
252-
DecisionResponse<Variation> decisionResponse = bucketToVariation(experiment, bucketingId);
253-
reasons.merge(decisionResponse.getReasons());
254-
return new DecisionResponse<>(decisionResponse.getResult(), reasons);
255-
}
175+
DecisionResponse<Variation> decisionResponse = bucketToVariation(experiment, bucketingId);
176+
reasons.merge(decisionResponse.getReasons());
177+
return new DecisionResponse<>(decisionResponse.getResult(), reasons);
256178
}
257179

258180
//======== Helper methods ========//
@@ -270,4 +192,4 @@ int generateBucketValue(int hashCode) {
270192
return (int) Math.floor(MAX_TRAFFIC_VALUE * ratio);
271193
}
272194

273-
}
195+
}

core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
158158
if (decisionMeetAudience.getResult()) {
159159
String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
160160
String cmabUUID = null;
161-
if (decisionPath == DecisionPath.WITH_CMAB && isCmabExperiment(experiment)) {
161+
decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig);
162+
if (decisionPath == DecisionPath.WITH_CMAB && isCmabExperiment(experiment) && decisionVariation.getResult() != null) {
163+
// group-allocation and traffic-allocation checking passed for cmab
164+
// we need server decision overruling local bucketing for cmab
162165
DecisionResponse<CmabDecision> cmabDecision = getDecisionForCmabExperiment(projectConfig, experiment, user, bucketingId, options);
163166
reasons.merge(cmabDecision.getReasons());
164167

@@ -174,7 +177,6 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
174177
}
175178
} else {
176179
// Standard bucketing for non-CMAB experiments
177-
decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig);
178180
reasons.merge(decisionVariation.getReasons());
179181
variation = decisionVariation.getResult();
180182
}
@@ -940,23 +942,6 @@ private DecisionResponse<CmabDecision> getDecisionForCmabExperiment(@Nonnull Pro
940942
@Nonnull List<OptimizelyDecideOption> options) {
941943
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
942944

943-
// Check if user is in CMAB traffic allocation
944-
DecisionResponse<Variation> bucketResponse = bucketer.bucket(experiment, bucketingId, projectConfig, true);
945-
// DecisionResponse<String> bucketResponse = bucketer.bucketForCmab(experiment, bucketingId, projectConfig);
946-
reasons.merge(bucketResponse.getReasons());
947-
948-
Variation bucketedVariation = bucketResponse.getResult();
949-
String bucketedEntityId = bucketedVariation != null ? bucketedVariation.getId() : null;
950-
951-
if (bucketedEntityId == null) {
952-
String message = String.format("User \"%s\" not in CMAB experiment \"%s\" due to traffic allocation.",
953-
userContext.getUserId(), experiment.getKey());
954-
logger.info(message);
955-
reasons.addInfo(message);
956-
957-
return new DecisionResponse<>(null, reasons);
958-
}
959-
960945
// User is in CMAB allocation, proceed to CMAB decision
961946
try {
962947
CmabDecision cmabDecision = cmabService.getDecision(projectConfig, userContext, experiment.getId(), options);

core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,9 +1535,13 @@ public void getVariationCmabExperimentServiceError() {
15351535
mockCmab // This makes it a CMAB experiment
15361536
);
15371537

1538-
Bucketer bucketer = new Bucketer();
1538+
// Bucketer bucketer = new Bucketer();
1539+
Bucketer mockBucketer = mock(Bucketer.class);
1540+
Variation bucketedVariation = new Variation("$", "$");
1541+
when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)))
1542+
.thenReturn(DecisionResponse.responseNoReasons(bucketedVariation));
15391543
DecisionService decisionServiceWithMockCmabService = new DecisionService(
1540-
bucketer,
1544+
mockBucketer,
15411545
mockErrorHandler,
15421546
null,
15431547
mockCmabService
@@ -1599,7 +1603,7 @@ public void getVariationCmabExperimentServiceSuccess() {
15991603
// Mock bucketer to return a variation (user is in CMAB traffic)
16001604
Variation bucketedVariation = new Variation("$", "$");
16011605
Bucketer mockBucketer = mock(Bucketer.class);
1602-
when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), eq(true)))
1606+
when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)))
16031607
.thenReturn(DecisionResponse.responseNoReasons(bucketedVariation));
16041608

16051609
DecisionService decisionServiceWithMockCmabService = new DecisionService(
@@ -1670,7 +1674,7 @@ public void getVariationCmabExperimentUserNotInTrafficAllocation() {
16701674

16711675
// Mock bucketer to return null for CMAB allocation (user not in CMAB traffic)
16721676
Bucketer mockBucketer = mock(Bucketer.class);
1673-
when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), eq(true)))
1677+
when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)))
16741678
.thenReturn(DecisionResponse.nullNoReasons());
16751679

16761680
DecisionService decisionServiceWithMockCmabService = new DecisionService(
@@ -1697,7 +1701,7 @@ public void getVariationCmabExperimentUserNotInTrafficAllocation() {
16971701
verify(mockCmabService, never()).getDecision(any(), any(), any(), any());
16981702

16991703
// Verify that bucketer was called for CMAB allocation
1700-
verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), eq(true));
1704+
verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class));
17011705
}
17021706

17031707
private Experiment createMockCmabExperiment() {

0 commit comments

Comments
 (0)