Skip to content

Commit c5fa5c0

Browse files
committed
Bug 1829014 - Re-evaluate bucketing when updating rollouts r=emcminn
Differential Revision: https://phabricator.services.mozilla.com/D176177 UltraBlame original commit: 57a5cee8e8143d46a4459b6a65642068177e5cdb
1 parent 6f58201 commit c5fa5c0

File tree

3 files changed

+136
-3
lines changed

3 files changed

+136
-3
lines changed

toolkit/components/nimbus/lib/ExperimentManager.sys.mjs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class _ExperimentManager {
160160
this.sessions.get(source).add(slug);
161161

162162
if (this.store.has(slug)) {
163-
this.updateEnrollment(recipe);
163+
await this.updateEnrollment(recipe);
164164
} else if (isEnrollmentPaused) {
165165
lazy.log.debug(`Enrollment is paused for "${slug}"`);
166166
} else if (!(await this.isInBucketAllocation(recipe.bucketConfig))) {
@@ -497,7 +497,7 @@ export class _ExperimentManager {
497497
* @param {RecipeArgs} recipe
498498
* @returns {boolean} whether the enrollment is still active
499499
*/
500-
updateEnrollment(recipe) {
500+
async updateEnrollment(recipe) {
501501
/** @type Enrollment */
502502
const enrollment = this.store.get(recipe.slug);
503503

@@ -507,6 +507,14 @@ export class _ExperimentManager {
507507
return false;
508508
}
509509

510+
if (
511+
recipe.isRollout &&
512+
!(await this.isInBucketAllocation(recipe.bucketConfig))
513+
) {
514+
this.unenroll(recipe.slug, "bucketing");
515+
return false;
516+
}
517+
510518
// Stay in the same branch, don't re-sample every time.
511519
const branch = recipe.branches.find(
512520
branch => branch.slug === enrollment.branch.slug

toolkit/components/nimbus/test/unit/test_ExperimentManager_lifecycle.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ add_task(async function test_onRecipe_rollout_update() {
215215
"should call .updateEnrollment() if the recipe has already been enrolled"
216216
);
217217
Assert.ok(
218-
manager.updateEnrollment.alwaysReturned(true),
218+
manager.updateEnrollment.alwaysReturned(Promise.resolve(true)),
219219
"updateEnrollment will confirm the enrolled branch still exists in the recipe and exit"
220220
);
221221
Assert.ok(

toolkit/components/nimbus/test/unit/test_RemoteSettingsExperimentLoader_updateRecipes.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,3 +973,128 @@ add_task(async function test_updateRecipes_invalidFeature_mismatch() {
973973

974974
targetingSpy.restore();
975975
});
976+
977+
add_task(async function test_updateRecipes_rollout_bucketing() {
978+
TelemetryEvents.init();
979+
Services.fog.testResetFOG();
980+
Services.telemetry.snapshotEvents(
981+
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
982+
true
983+
);
984+
985+
const loader = ExperimentFakes.rsLoader();
986+
const manager = loader.manager;
987+
988+
const experiment = ExperimentFakes.recipe("experiment", {
989+
branches: [
990+
{
991+
slug: "control",
992+
ratio: 1,
993+
features: [
994+
{
995+
featureId: "testFeature",
996+
value: {},
997+
},
998+
],
999+
},
1000+
],
1001+
bucketConfig: {
1002+
namespace: "nimbus-test-utils",
1003+
randomizationUnit: "normandy_id",
1004+
start: 0,
1005+
count: 1000,
1006+
total: 1000,
1007+
},
1008+
});
1009+
const rollout = ExperimentFakes.recipe("rollout", {
1010+
isRollout: true,
1011+
branches: [
1012+
{
1013+
slug: "rollout",
1014+
ratio: 1,
1015+
features: [
1016+
{
1017+
featureId: "testFeature",
1018+
value: {},
1019+
},
1020+
],
1021+
},
1022+
],
1023+
bucketConfig: {
1024+
namespace: "nimbus-test-utils",
1025+
randomizationUnit: "normandy_id",
1026+
start: 0,
1027+
count: 1000,
1028+
total: 1000,
1029+
},
1030+
});
1031+
1032+
await loader.init();
1033+
await manager.onStartup();
1034+
await manager.store.ready();
1035+
1036+
sinon
1037+
.stub(loader.remoteSettingsClient, "get")
1038+
.resolves([experiment, rollout]);
1039+
1040+
await loader.updateRecipes();
1041+
1042+
Assert.equal(
1043+
manager.store.getExperimentForFeature("testFeature")?.slug,
1044+
experiment.slug,
1045+
"Should enroll in experiment"
1046+
);
1047+
Assert.equal(
1048+
manager.store.getRolloutForFeature("testFeature")?.slug,
1049+
rollout.slug,
1050+
"Should enroll in rollout"
1051+
);
1052+
1053+
experiment.bucketConfig.count = 0;
1054+
rollout.bucketConfig.count = 0;
1055+
1056+
await loader.updateRecipes();
1057+
1058+
Assert.equal(
1059+
manager.store.getExperimentForFeature("testFeature")?.slug,
1060+
experiment.slug,
1061+
"Should stay enrolled in experiment -- experiments cannot be resized"
1062+
);
1063+
Assert.ok(
1064+
!manager.store.getRolloutForFeature("testFeature"),
1065+
"Should unenroll from rollout"
1066+
);
1067+
1068+
const unenrollmentEvents = Glean.nimbusEvents.unenrollment.testGetValue();
1069+
Assert.equal(
1070+
unenrollmentEvents.length,
1071+
1,
1072+
"Should be one unenrollment event"
1073+
);
1074+
Assert.equal(
1075+
unenrollmentEvents[0].extra.experiment,
1076+
rollout.slug,
1077+
"Experiment slug should match"
1078+
);
1079+
Assert.equal(
1080+
unenrollmentEvents[0].extra.reason,
1081+
"bucketing",
1082+
"Reason should match"
1083+
);
1084+
1085+
TelemetryTestUtils.assertEvents(
1086+
[
1087+
{
1088+
value: rollout.slug,
1089+
extra: {
1090+
reason: "bucketing",
1091+
},
1092+
},
1093+
],
1094+
{
1095+
category: "normandy",
1096+
method: "unenroll",
1097+
object: "nimbus_experiment",
1098+
}
1099+
);
1100+
});

0 commit comments

Comments
 (0)