Skip to content

Commit 91e0427

Browse files
author
Josh Deffibaugh
committed
Changes persistent bucketer to user experiment record, fixes whitespace,
typos, naming, etc from PR review
1 parent 032435a commit 91e0427

File tree

7 files changed

+111
-56
lines changed

7 files changed

+111
-56
lines changed

core-api/src/main/java/com/optimizely/ab/Optimizely.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private Optimizely(@Nonnull ProjectConfig projectConfig,
9696

9797
// Do work here that should be done once per Optimizely lifecycle
9898
@VisibleForTesting void initialize() {
99-
bucketer.cleanUserExperimentRecord();
99+
bucketer.cleanUserExperimentRecords();
100100
}
101101

102102
//======== activate calls ========//
@@ -457,9 +457,7 @@ protected Builder withEventBuilder(EventBuilder eventBuilder) {
457457
return this;
458458
}
459459

460-
/**
461-
* Helper function for making testing easier
462-
*/
460+
// Helper function for making testing easier
463461
protected Builder withConfig(ProjectConfig projectConfig) {
464462
this.projectConfig = projectConfig;
465463
return this;

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

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,22 +108,30 @@ private Experiment bucketToExperiment(@Nonnull Group group,
108108
private Variation bucketToVariation(@Nonnull Experiment experiment,
109109
@Nonnull String userId) {
110110
// "salt" the bucket id using the experiment id
111-
String combinedBucketId = userId + experiment.getId();
111+
String experimentId = experiment.getId();
112+
String experimentKey = experiment.getKey();
113+
String combinedBucketId = userId + experimentId;
112114

113-
// If a persistent bucketer instance is present then check it for a persisted variation
115+
// If a user experiment record instance is present then check it for a saved variation
114116
if (userExperimentRecord != null) {
115-
String variationKey = userExperimentRecord.lookup(userId, experiment.getKey());
117+
String variationKey = userExperimentRecord.lookup(userId, experimentKey);
116118
if (variationKey != null) {
117-
logger.info("Returning previously activated variation \"{}\" from user experiment registry.", variationKey);
119+
logger.info("Returning previously activated variation \"{}\" of experiment \"{}\" "
120+
+ "for user \"{}\" from user experiment record.",
121+
variationKey, experimentKey, userId);
118122
// A variation is stored for this combined bucket id
119-
return projectConfig.getExperimentIdMapping().get(experiment.getId()).getVariationKeyToVariationMap().get(variationKey);
123+
return projectConfig
124+
.getExperimentIdMapping()
125+
.get(experimentId)
126+
.getVariationKeyToVariationMap()
127+
.get(variationKey);
120128
} else {
121-
logger.info("No previously activated variation returned from persistent bucketer.");
129+
logger.info("No previously activated variation of experiment \"{}\" "
130+
+ "for user \"{}\" found in user experiment record.",
131+
experimentKey, userId);
122132
}
123133
}
124134

125-
String experimentKey = experiment.getKey();
126-
127135
List<TrafficAllocation> trafficAllocations = experiment.getTrafficAllocation();
128136

129137
int hashCode = MurmurHash3.murmurhash3_x86_32(combinedBucketId, 0, combinedBucketId.length(), MURMUR_HASH_SEED);
@@ -133,16 +141,19 @@ private Variation bucketToVariation(@Nonnull Experiment experiment,
133141
String bucketedVariationId = bucketToEntity(bucketValue, trafficAllocations);
134142
if (bucketedVariationId != null) {
135143
Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId);
136-
logger.info("User \"{}\" is in variation \"{}\" of experiment \"{}\".", userId, bucketedVariation.getKey(),
144+
String variationKey = bucketedVariation.getKey();
145+
logger.info("User \"{}\" is in variation \"{}\" of experiment \"{}\".", userId, variationKey,
137146
experimentKey);
138147

139-
// If a persistent bucketer instance is present give it a variation to store
148+
// If a user experiment record is present give it a variation to store
140149
if (userExperimentRecord != null) {
141-
boolean saved = userExperimentRecord.save(userId, experiment.getKey(), bucketedVariation.getKey());
150+
boolean saved = userExperimentRecord.save(userId, experiment.getKey(), variationKey);
142151
if (saved) {
143-
logger.info("Persisted variation \"{}\" of experiment \"{}\".", bucketedVariation.getKey(), experimentKey);
152+
logger.info("Saved variation \"{}\" of experiment \"{}\" for user \"{}\".",
153+
variationKey, experimentKey, userId);
144154
} else {
145-
logger.warn("Failed to persist variation \"{}\" of experiment \"{}\".", bucketedVariation.getKey(), experimentKey);
155+
logger.warn("Failed to save variation \"{}\" of experiment \"{}\" for user \"{}\".",
156+
variationKey, experimentKey, userId);
146157
}
147158
}
148159

@@ -221,9 +232,9 @@ public UserExperimentRecord getUserExperimentRecord() {
221232
* Gives implementations of {@link UserExperimentRecord} a chance to remove records
222233
* of experiments that are deleted or not running.
223234
*/
224-
public void cleanUserExperimentRecord() {
235+
public void cleanUserExperimentRecords() {
225236
if (userExperimentRecord != null) {
226-
Map<String,Map<String,String>> records = userExperimentRecord.records();
237+
Map<String, Map<String,String>> records = userExperimentRecord.getAllRecords();
227238
if (records != null) {
228239
for (Map.Entry<String,Map<String,String>> record : records.entrySet()) {
229240
for (String experimentKey : record.getValue().keySet()) {

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/**
2+
*
3+
* Copyright 2016, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
117
package com.optimizely.ab.bucketing;
218

319
import java.util.Map;
@@ -23,6 +39,7 @@ public interface UserExperimentRecord {
2339

2440
/**
2541
* Called by the bucketer to check for a record of the previous activation
42+
*
2643
* @param userId the user is id of the next activation
2744
* @param experimentKey the experiment id of the next activation
2845
* @return the variation key of the next activation, or null if no record exists
@@ -44,10 +61,10 @@ public interface UserExperimentRecord {
4461
/**
4562
* Called by bucketer to get a mapping of all stored records
4663
*
47-
* This mapping is needed so that the bucketer can {@link this#remove(String, String)} outdated
64+
* This mapping is needed so that the bucketer can {@link #remove(String, String)} outdated
4865
* records.
4966
* @return a map of userIds to a map of experiment keys to variation keys
5067
*/
51-
Map<String,Map<String,String>> records();
68+
Map<String, Map<String,String>> getAllRecords();
5269

5370
}

core-api/src/main/java/com/optimizely/ab/event/EventHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.Map;
2020

2121
/**
22-
* Implementations are responsible for dispatching {@link LogEvent}s to the Optimizely event end-point.
22+
* Implementations are responsible for dispatching event's to the Optimizely event end-point.
2323
*/
2424
public interface EventHandler {
2525

core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public void withDefaultErrorHandler() throws Exception {
8888
}
8989

9090
@Test
91-
public void withPersistentBucketer() throws Exception {
91+
public void withUserExperimentRecord() throws Exception {
9292
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
9393
Optimizely optimizelyClient = Optimizely.builder(validConfigJson(), mockEventHandler)
9494
.withUserExperimentRecord(userExperimentRecord)

core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public void initializationOccursForBucketerWhenBuildingOptly() throws Exception
155155
.withErrorHandler(mockErrorHandler)
156156
.build();
157157

158-
verify(mockBucketer).cleanUserExperimentRecord();
158+
verify(mockBucketer).cleanUserExperimentRecords();
159159
}
160160

161161
/**

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

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,6 @@ public void bucketUserInForcedVariation() throws Exception {
252252
assertThat(algorithm.bucket(experiment, "testUser1").getKey(), is("var1"));
253253
}
254254

255-
256-
257255
/**
258256
* Verify that {@link Bucketer#bucket(Experiment, String)} returns the proper variation when the user doesn't
259257
* have a forced variation mapping.
@@ -447,10 +445,14 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception {
447445
assertNull(algorithm.bucket(groupExperiment, "blah"));
448446
}
449447

450-
@Test public void bucketUserSaveActivationWithPersistentBucketer() throws Exception {
448+
/**
449+
* Verify that {@link Bucketer#bucket(Experiment,String} saves a variation of an experiment for a user
450+
* when a {@link UserExperimentRecord} is present.
451+
*/
452+
@Test public void bucketUserSaveActivationWithUserExperimentRecord() throws Exception {
451453
final AtomicInteger bucketValue = new AtomicInteger();
452454
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
453-
Bucketer algorithm = mockPersistentBucketAlgorithm(bucketValue, userExperimentRecord);
455+
Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord);
454456
bucketValue.set(3000);
455457

456458
ProjectConfig projectConfig = validProjectConfig();
@@ -463,15 +465,19 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception {
463465
assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation));
464466

465467
logbackVerifier.expectMessage(Level.INFO,
466-
"Persisted variation \"e2_vtag1\" of experiment \"group_etag2\".");
468+
"Saved variation \"e2_vtag1\" of experiment \"group_etag2\" for user \"blah\".");
467469

468470
verify(userExperimentRecord).save("blah", groupExperiment.getKey(), variation.getKey());
469471
}
470472

471-
@Test public void bucketUserSaveActivationFailWithPersistentBucketer() throws Exception {
473+
/**
474+
* Verify that {@link Bucketer#bucket(Experiment,String} logs correctly
475+
* when a {@link UserExperimentRecord} is present and fails to save an activation.
476+
*/
477+
@Test public void bucketUserSaveActivationFailWithUserExperimentRecord() throws Exception {
472478
final AtomicInteger bucketValue = new AtomicInteger();
473479
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
474-
Bucketer algorithm = mockPersistentBucketAlgorithm(bucketValue, userExperimentRecord);
480+
Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord);
475481
bucketValue.set(3000);
476482

477483
ProjectConfig projectConfig = validProjectConfig();
@@ -484,15 +490,19 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception {
484490
assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation));
485491

486492
logbackVerifier.expectMessage(Level.WARN,
487-
"Failed to persist variation \"e2_vtag1\" of experiment \"group_etag2\".");
493+
"Failed to save variation \"e2_vtag1\" of experiment \"group_etag2\" for user \"blah\".");
488494

489495
verify(userExperimentRecord).save("blah", groupExperiment.getKey(), variation.getKey());
490496
}
491497

492-
@Test public void bucketUserRestoreActivationWithPersistentBucketer() throws Exception {
498+
/**
499+
* Verify that {@link Bucketer#bucket(Experiment,String)} returns a variation that is
500+
* stored in the provided {@link userExperimentRecord}.
501+
*/
502+
@Test public void bucketUserRestoreActivationWithUserExperimentRecord() throws Exception {
493503
final AtomicInteger bucketValue = new AtomicInteger();
494504
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
495-
Bucketer algorithm = mockPersistentBucketAlgorithm(bucketValue, userExperimentRecord);
505+
Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord);
496506
bucketValue.set(3000);
497507

498508
ProjectConfig projectConfig = validProjectConfig();
@@ -505,15 +515,20 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception {
505515
assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation));
506516

507517
logbackVerifier.expectMessage(Level.INFO,
508-
"Returning previously activated variation \"e2_vtag1\" from user experiment registry.");
518+
"Returning previously activated variation \"e2_vtag1\" of experiment \"group_etag2\""
519+
+ " for user \"blah\" from user experiment record.");
509520

510521
verify(userExperimentRecord).lookup("blah", groupExperiment.getKey());
511522
}
512523

513-
@Test public void bucketUserRestoreActivationNullWithPersistentBucketer() throws Exception {
524+
/**
525+
* Verify {@link Bucketer#bucket(Experiment,String)} handles a present {@link UserExperimentRecord}
526+
* returning null when looking up a variation.
527+
*/
528+
@Test public void bucketUserRestoreActivationNullWithUserExperimentRecord() throws Exception {
514529
final AtomicInteger bucketValue = new AtomicInteger();
515530
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
516-
Bucketer algorithm = mockPersistentBucketAlgorithm(bucketValue, userExperimentRecord);
531+
Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord);
517532
bucketValue.set(3000);
518533

519534
ProjectConfig projectConfig = validProjectConfig();
@@ -525,73 +540,87 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception {
525540

526541
assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation));
527542

528-
logbackVerifier.expectMessage(Level.INFO,
529-
"No previously activated variation returned from persistent bucketer.");
530-
543+
logbackVerifier.expectMessage(Level.INFO, "No previously activated variation of experiment " +
544+
"\"group_etag2\" for user \"blah\" found in user experiment record.");
531545
verify(userExperimentRecord).lookup("blah", groupExperiment.getKey());
532546
}
533547

548+
/**
549+
* Verify {@link Bucketer#cleanUserExperimentRecords()} handles a null {@link UserExperimentRecord}.
550+
*/
534551
@Test
535552
public void nullUserExperimentRecordWhenCleaning() {
536553
final AtomicInteger bucketValue = new AtomicInteger();
537554
Bucketer algorithm = mockBucketAlgorithm(bucketValue);
538555
bucketValue.set(3000);
539556
try {
540-
algorithm.cleanUserExperimentRecord();
557+
algorithm.cleanUserExperimentRecords();
541558
} catch (NullPointerException e) {
542559
fail();
543560
}
544561
}
545562

563+
/**
564+
* Verify {@link Bucketer#cleanUserExperimentRecords()} handles a null returned from
565+
* {@link UserExperimentRecord#getAllRecords()}.
566+
*/
546567
@Test
547568
public void nullUserExperimentRecords() {
548569
final AtomicInteger bucketValue = new AtomicInteger();
549570
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
550-
Bucketer algorithm = mockPersistentBucketAlgorithm(bucketValue, userExperimentRecord);
571+
Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord);
551572
bucketValue.set(3000);
552573

553-
when(userExperimentRecord.records()).thenReturn(null);
574+
when(userExperimentRecord.getAllRecords()).thenReturn(null);
554575
try {
555-
algorithm.cleanUserExperimentRecord();
576+
algorithm.cleanUserExperimentRecords();
556577
} catch (NullPointerException e) {
557578
fail();
558579
}
559580
}
560581

582+
/**
583+
* Verify {@link Bucketer#cleanUserExperimentRecords()} removes experiments
584+
* that are no longer in the {@link ProjectConfig}.
585+
*/
561586
@Test
562587
public void cleanRemovesRecordsOfExperimentsThatNoLongerExist() {
563588
final AtomicInteger bucketValue = new AtomicInteger();
564589
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
565-
Bucketer algorithm = mockPersistentBucketAlgorithm(bucketValue, userExperimentRecord);
590+
Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord);
566591
bucketValue.set(3000);
567592

568593
Map<String,Map<String,String>> records = new HashMap<String, Map<String, String>>();
569594
Map<String,String> activation = new HashMap<String, String>();
570-
activation.put("57", "57");
595+
activation.put("exp1", "var1");
571596
records.put("blah", activation);
572-
when(userExperimentRecord.records()).thenReturn(records);
597+
when(userExperimentRecord.getAllRecords()).thenReturn(records);
573598

574-
algorithm.cleanUserExperimentRecord();
599+
algorithm.cleanUserExperimentRecords();
575600

576-
verify(userExperimentRecord).remove("blah", "57");
601+
verify(userExperimentRecord).remove("blah", "exp1");
577602
}
578603

604+
/**
605+
* Verify {@link Bucketer#cleanUserExperimentRecords()} removes experiments
606+
* that are paused in the {@link ProjectConfig}.
607+
*/
579608
@Test
580609
public void cleanRemovesRecordsOfExperimentsThatAreNotRunning() {
581610
final AtomicInteger bucketValue = new AtomicInteger();
582611
UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class);
583-
Bucketer algorithm = mockPersistentBucketAlgorithm(bucketValue, userExperimentRecord);
612+
Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord);
584613
bucketValue.set(3000);
585614

586615
Map<String,Map<String,String>> records = new HashMap<String, Map<String, String>>();
587616
Map<String,String> activation = new HashMap<String, String>();
588-
activation.put("118", "57");
617+
activation.put("exp1", "var1");
589618
records.put("blah", activation);
590-
when(userExperimentRecord.records()).thenReturn(records);
619+
when(userExperimentRecord.getAllRecords()).thenReturn(records);
591620

592-
algorithm.cleanUserExperimentRecord();
621+
algorithm.cleanUserExperimentRecords();
593622

594-
verify(userExperimentRecord).remove("blah", "118");
623+
verify(userExperimentRecord).remove("blah", "exp1");
595624
}
596625

597626
//======== Helper methods ========//
@@ -618,12 +647,12 @@ int generateBucketValue(int hashCode) {
618647
* @param bucketValue the expected bucket value holder
619648
* @return the mock bucket algorithm
620649
*/
621-
private Bucketer mockPersistentBucketAlgorithm(final AtomicInteger bucketValue, final UserExperimentRecord userExperimentRecord) {
650+
private Bucketer mockUserExperimentRecordAlgorithm(final AtomicInteger bucketValue, final UserExperimentRecord userExperimentRecord) {
622651
return new Bucketer(validProjectConfig(), userExperimentRecord) {
623652
@Override
624653
int generateBucketValue(int hashCode) {
625654
return bucketValue.get();
626655
}
627656
};
628657
}
629-
}
658+
}

0 commit comments

Comments
 (0)