Skip to content

Commit 54bb92b

Browse files
committed
feat(nimbus): add state machine for rollouts
1 parent cedc318 commit 54bb92b

14 files changed

Lines changed: 1979 additions & 44 deletions

File tree

docs/experimenter/openapi-schema.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@
177177
"Draft",
178178
"Preview",
179179
"Live",
180-
"Complete"
180+
"Complete",
181+
"Disabled"
181182
]
182183
}
183184
},
@@ -273,7 +274,8 @@
273274
"Draft",
274275
"Preview",
275276
"Live",
276-
"Complete"
277+
"Complete",
278+
"Disabled"
277279
]
278280
}
279281
},
@@ -621,7 +623,8 @@
621623
"Draft",
622624
"Preview",
623625
"Live",
624-
"Complete"
626+
"Complete",
627+
"Disabled"
625628
]
626629
}
627630
},
@@ -699,7 +702,8 @@
699702
"Draft",
700703
"Preview",
701704
"Live",
702-
"Complete"
705+
"Complete",
706+
"Disabled"
703707
]
704708
}
705709
},
@@ -783,7 +787,8 @@
783787
"Draft",
784788
"Preview",
785789
"Live",
786-
"Complete"
790+
"Complete",
791+
"Disabled"
787792
]
788793
}
789794
},
@@ -888,7 +893,8 @@
888893
"Draft",
889894
"Preview",
890895
"Live",
891-
"Complete"
896+
"Complete",
897+
"Disabled"
892898
]
893899
}
894900
},
@@ -1287,7 +1293,8 @@
12871293
"Draft",
12881294
"Preview",
12891295
"Live",
1290-
"Complete"
1296+
"Complete",
1297+
"Disabled"
12911298
],
12921299
"type": "string"
12931300
},

docs/experimenter/swagger-ui.html

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@
189189
"Draft",
190190
"Preview",
191191
"Live",
192-
"Complete"
192+
"Complete",
193+
"Disabled"
193194
]
194195
}
195196
},
@@ -285,7 +286,8 @@
285286
"Draft",
286287
"Preview",
287288
"Live",
288-
"Complete"
289+
"Complete",
290+
"Disabled"
289291
]
290292
}
291293
},
@@ -633,7 +635,8 @@
633635
"Draft",
634636
"Preview",
635637
"Live",
636-
"Complete"
638+
"Complete",
639+
"Disabled"
637640
]
638641
}
639642
},
@@ -711,7 +714,8 @@
711714
"Draft",
712715
"Preview",
713716
"Live",
714-
"Complete"
717+
"Complete",
718+
"Disabled"
715719
]
716720
}
717721
},
@@ -795,7 +799,8 @@
795799
"Draft",
796800
"Preview",
797801
"Live",
798-
"Complete"
802+
"Complete",
803+
"Disabled"
799804
]
800805
}
801806
},
@@ -900,7 +905,8 @@
900905
"Draft",
901906
"Preview",
902907
"Live",
903-
"Complete"
908+
"Complete",
909+
"Disabled"
904910
]
905911
}
906912
},
@@ -1299,7 +1305,8 @@
12991305
"Draft",
13001306
"Preview",
13011307
"Live",
1302-
"Complete"
1308+
"Complete",
1309+
"Disabled"
13031310
],
13041311
"type": "string"
13051312
},

experimenter/experimenter/experiments/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,10 @@ class Status(models.TextChoices):
462462
LIVE = "Live"
463463
COMPLETE = "Complete"
464464

465+
# This status applies only to rollouts, and indicates that the rollout has been
466+
# disabled. It is not a valid status for experiments.
467+
DISABLED = "Disabled"
468+
465469
class PublishStatus(models.TextChoices):
466470
IDLE = "Idle"
467471
REVIEW = "Review"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 5.2.15 on 2026-07-15 18:14
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('experiments', '0333_nimbusrolloutplantemplate_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='nimbuschangelog',
15+
name='new_status',
16+
field=models.CharField(choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255),
17+
),
18+
migrations.AlterField(
19+
model_name='nimbuschangelog',
20+
name='new_status_next',
21+
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
22+
),
23+
migrations.AlterField(
24+
model_name='nimbuschangelog',
25+
name='old_status',
26+
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
27+
),
28+
migrations.AlterField(
29+
model_name='nimbuschangelog',
30+
name='old_status_next',
31+
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
32+
),
33+
migrations.AlterField(
34+
model_name='nimbusexperiment',
35+
name='status',
36+
field=models.CharField(choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], default='Draft', max_length=255, verbose_name='Status'),
37+
),
38+
migrations.AlterField(
39+
model_name='nimbusexperiment',
40+
name='status_next',
41+
field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Preview', 'Preview'), ('Live', 'Live'), ('Complete', 'Complete'), ('Disabled', 'Disabled')], max_length=255, null=True),
42+
),
43+
]

experimenter/experimenter/experiments/models.py

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -545,13 +545,27 @@ class Meta:
545545

546546
class Filters:
547547
IS_LAUNCH_QUEUED = Q(
548-
status=NimbusConstants.Status.DRAFT,
549-
status_next=NimbusConstants.Status.LIVE,
548+
Q(
549+
status=NimbusConstants.Status.DRAFT,
550+
status_next=NimbusConstants.Status.LIVE,
551+
)
552+
| Q(
553+
is_rollout=True,
554+
status=NimbusConstants.Status.DISABLED,
555+
status_next=NimbusConstants.Status.LIVE,
556+
),
550557
publish_status=NimbusConstants.PublishStatus.APPROVED,
551558
)
552559
IS_LAUNCHING = Q(
553-
status=NimbusConstants.Status.DRAFT,
554-
status_next=NimbusConstants.Status.LIVE,
560+
Q(
561+
status=NimbusConstants.Status.DRAFT,
562+
status_next=NimbusConstants.Status.LIVE,
563+
)
564+
| Q(
565+
is_rollout=True,
566+
status=NimbusConstants.Status.DISABLED,
567+
status_next=NimbusConstants.Status.LIVE,
568+
),
555569
publish_status=NimbusConstants.PublishStatus.WAITING,
556570
)
557571
IS_UPDATE_QUEUED = Q(
@@ -565,13 +579,27 @@ class Filters:
565579
publish_status=NimbusConstants.PublishStatus.WAITING,
566580
)
567581
IS_END_QUEUED = Q(
568-
status=NimbusConstants.Status.LIVE,
569-
status_next=NimbusConstants.Status.COMPLETE,
582+
Q(
583+
status=NimbusConstants.Status.LIVE,
584+
status_next=NimbusConstants.Status.COMPLETE,
585+
)
586+
| Q(
587+
is_rollout=True,
588+
status=NimbusConstants.Status.LIVE,
589+
status_next=NimbusConstants.Status.DISABLED,
590+
),
570591
publish_status=NimbusConstants.PublishStatus.APPROVED,
571592
)
572593
IS_ENDING = Q(
573-
status=NimbusConstants.Status.LIVE,
574-
status_next=NimbusConstants.Status.COMPLETE,
594+
Q(
595+
status=NimbusConstants.Status.LIVE,
596+
status_next=NimbusConstants.Status.COMPLETE,
597+
)
598+
| Q(
599+
is_rollout=True,
600+
status=NimbusConstants.Status.LIVE,
601+
status_next=NimbusConstants.Status.DISABLED,
602+
),
575603
publish_status=NimbusConstants.PublishStatus.WAITING,
576604
)
577605
SHOULD_ALLOCATE_BUCKETS = Q(
@@ -1285,7 +1313,10 @@ def advance_rollout_phase(self):
12851313
today = timezone.now().date()
12861314
phase_ids = [phase.id for phase in phases]
12871315

1288-
if self.rollout_phase_id is None:
1316+
if self.rollout_phase_next_id is not None:
1317+
current_phase = self.rollout_phase
1318+
next_phase = self.rollout_phase_next
1319+
elif self.rollout_phase_id is None:
12891320
current_phase = None
12901321
next_phase = phases[0]
12911322
else:
@@ -1297,7 +1328,9 @@ def advance_rollout_phase(self):
12971328
if next_phase is not None and not next_phase.population_percent:
12981329
return
12991330

1300-
if current_phase is not None:
1331+
if current_phase is not None and (
1332+
current_phase.end_date is None or current_phase.end_date > today
1333+
):
13011334
current_phase.end_date = today
13021335
if current_phase.actual_start_date:
13031336
current_phase.start_date = current_phase.actual_start_date
@@ -1312,8 +1345,52 @@ def advance_rollout_phase(self):
13121345
next_phase.save()
13131346
self.rollout_phase = next_phase
13141347
self.rollout_phase_next = None
1348+
self.population_percent = next_phase.population_percent
13151349
self.save()
13161350

1351+
def stage_rollout_phase_advance(self, copy_current_if_missing=False):
1352+
phases = list(self.rollout_phases.all())
1353+
if not phases:
1354+
return None
1355+
1356+
current_phase = self.rollout_phase
1357+
if self.rollout_phase_next_id is not None:
1358+
next_phase = self.rollout_phase_next
1359+
elif self.rollout_phase_id is None:
1360+
next_phase = phases[0]
1361+
else:
1362+
phase_ids = [phase.id for phase in phases]
1363+
current_index = phase_ids.index(self.rollout_phase_id)
1364+
next_index = current_index + 1
1365+
next_phase = phases[next_index] if next_index < len(phases) else None
1366+
1367+
if next_phase is None and copy_current_if_missing and current_phase is not None:
1368+
next_phase = self.rollout_phases.create(
1369+
population_percent=current_phase.population_percent
1370+
)
1371+
1372+
if next_phase is None or not next_phase.population_percent:
1373+
return None
1374+
1375+
self.rollout_phase_next = next_phase
1376+
self.population_percent = next_phase.population_percent
1377+
self.save(update_fields=["rollout_phase_next", "population_percent"])
1378+
return next_phase
1379+
1380+
def end_current_rollout_phase(self):
1381+
current_phase = self.rollout_phase
1382+
if current_phase is None:
1383+
return
1384+
1385+
today = timezone.now().date()
1386+
if current_phase.end_date is not None and current_phase.end_date <= today:
1387+
return
1388+
1389+
current_phase.end_date = today
1390+
if current_phase.actual_start_date:
1391+
current_phase.start_date = current_phase.actual_start_date
1392+
current_phase.save()
1393+
13171394
@property
13181395
def is_missing_takeaway_info(self):
13191396
return (
@@ -3457,6 +3534,7 @@ class Messages:
34573534
REJECTED_FROM_KINTO = "Rejected from Remote Settings"
34583535
LIVE = "Experiment is live"
34593536
COMPLETED = "Experiment is complete"
3537+
DISABLED = "Rollout is disabled"
34603538
RESULTS_UPDATED = "Experiment results updated"
34613539
MONITORING_DATA_UPDATED = "Experiment monitoring data updated"
34623540
HOLDBACK_ENROLLMENT_UPDATED = "Holdback enrollment period updated"

0 commit comments

Comments
 (0)