|
19 | 19 | SCHEDULED_RECALCULATION_WORKFLOW_NAME, |
20 | 20 | ExperimentPrecomputeCanaryInputs, |
21 | 21 | ExperimentPrecomputeEnrollmentCensusInputs, |
22 | | - ScheduledRecalculationWorkflowInputs, |
23 | 22 | ) |
24 | 23 |
|
25 | 24 | CANARY_SCHEDULE_ID = "experiment-precompute-canary-schedule" |
@@ -79,42 +78,57 @@ async def create_experiment_precompute_enrollment_census_schedule(client: Client |
79 | 78 | await a_create_schedule(client, ENROLLMENT_CENSUS_SCHEDULE_ID, schedule, trigger_immediately=False) |
80 | 79 |
|
81 | 80 |
|
82 | | -SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX = "experiment-scheduled-recalculation-hour" |
| 81 | +SCHEDULED_RECALCULATION_SCHEDULE_ID = "experiment-scheduled-recalculation" |
| 82 | + |
| 83 | +# Each hour had its own schedule before the workflow resolved the hour itself. Temporal keeps a |
| 84 | +# schedule until it is deleted, so the replaced ones are removed when the single one is created. |
| 85 | +LEGACY_HOURLY_SCHEDULE_ID_PREFIX = "experiment-scheduled-recalculation-hour" |
83 | 86 |
|
84 | 87 |
|
85 | 88 | async def create_experiment_scheduled_recalculation_schedules(client: Client) -> None: |
86 | | - """Create or update 24 schedules, one per hour, that start a real metrics recalculation for |
87 | | - every eligible experiment belonging to teams configured for that hour. |
| 89 | + """Create or update the hourly schedule that starts a real metrics recalculation for every |
| 90 | + experiment eligible in the hour it fires. |
| 91 | +
|
| 92 | + Fires at :30, after the daily timeseries schedule at :00, so the timeseries sync publish |
| 93 | + usually lands before a real run supersedes it. The workflow takes no input: discovery reads |
| 94 | + the hour and selects the teams configured for it, so one schedule serves all 24 hours. |
88 | 95 |
|
89 | | - Each fires at :30, after the daily timeseries schedule at :00, so the timeseries sync publish |
90 | | - usually lands before a real run supersedes it. 24 separate schedules rather than one hourly |
91 | | - schedule, matching the timeseries workflows: a run longer than an hour cannot overlap itself, |
92 | | - so no overlap policy is needed. |
| 96 | + SKIP overlap: a run that outlives its hour must not stack on the next one. |
93 | 97 | """ |
94 | | - for hour in range(24): |
95 | | - schedule_id = f"{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}" |
| 98 | + schedule = Schedule( |
| 99 | + action=ScheduleActionStartWorkflow( |
| 100 | + SCHEDULED_RECALCULATION_WORKFLOW_NAME, |
| 101 | + id=f'{SCHEDULED_RECALCULATION_SCHEDULE_ID}-{{{{.ScheduledTime.Format "2006-01-02T15"}}}}', |
| 102 | + task_queue=settings.GENERAL_PURPOSE_TASK_QUEUE, |
| 103 | + ), |
| 104 | + spec=ScheduleSpec(cron_expressions=["30 * * * *"]), |
| 105 | + policy=SchedulePolicy(overlap=ScheduleOverlapPolicy.SKIP), |
| 106 | + ) |
| 107 | + |
| 108 | + if await a_schedule_exists(client, SCHEDULED_RECALCULATION_SCHEDULE_ID): |
| 109 | + await a_update_schedule(client, SCHEDULED_RECALCULATION_SCHEDULE_ID, schedule) |
| 110 | + else: |
| 111 | + await a_create_schedule(client, SCHEDULED_RECALCULATION_SCHEDULE_ID, schedule, trigger_immediately=False) |
96 | 112 |
|
97 | | - schedule = Schedule( |
98 | | - action=ScheduleActionStartWorkflow( |
99 | | - SCHEDULED_RECALCULATION_WORKFLOW_NAME, |
100 | | - ScheduledRecalculationWorkflowInputs(hour=hour), |
101 | | - id=f'{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}-{{{{.ScheduledTime.Format "2006-01-02"}}}}', |
102 | | - task_queue=settings.GENERAL_PURPOSE_TASK_QUEUE, |
103 | | - ), |
104 | | - spec=ScheduleSpec(cron_expressions=[f"30 {hour} * * *"]), |
105 | | - policy=SchedulePolicy(overlap=ScheduleOverlapPolicy.SKIP), |
106 | | - ) |
| 113 | + await _delete_legacy_hourly_schedules(client) |
107 | 114 |
|
108 | | - if await a_schedule_exists(client, schedule_id): |
109 | | - await a_update_schedule(client, schedule_id, schedule) |
110 | | - else: |
111 | | - await a_create_schedule(client, schedule_id, schedule, trigger_immediately=False) |
112 | 115 |
|
| 116 | +async def _delete_legacy_hourly_schedules(client: Client) -> None: |
| 117 | + """Remove the 24 per-hour schedules the single schedule replaces. |
113 | 118 |
|
114 | | -async def delete_experiment_scheduled_recalculation_schedules(client: Client) -> None: |
115 | | - """Delete all 24 scheduled recalculation schedules.""" |
| 119 | + Without this they keep firing alongside it, starting the workflow 25 times an hour. |
| 120 | + """ |
116 | 121 | for hour in range(24): |
117 | 122 | try: |
118 | | - await a_delete_schedule(client, f"{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}") |
| 123 | + await a_delete_schedule(client, f"{LEGACY_HOURLY_SCHEDULE_ID_PREFIX}-{hour:02d}") |
119 | 124 | except Exception: |
120 | | - pass # Schedule might not exist |
| 125 | + pass # Already gone, which is the steady state after the first run. |
| 126 | + |
| 127 | + |
| 128 | +async def delete_experiment_scheduled_recalculation_schedules(client: Client) -> None: |
| 129 | + """Delete the scheduled recalculation schedule, and any legacy per-hour ones left behind.""" |
| 130 | + try: |
| 131 | + await a_delete_schedule(client, SCHEDULED_RECALCULATION_SCHEDULE_ID) |
| 132 | + except Exception: |
| 133 | + pass # Schedule might not exist |
| 134 | + await _delete_legacy_hourly_schedules(client) |
0 commit comments