Skip to content

feat(experiments): schedule experiment recalculations hourly - #108101

Open
rodrigoi wants to merge 8 commits into
experiment/scheduled-recalc-workflowfrom
experiment/scheduled-recalc-schedules
Open

rodrigoi wants to merge 8 commits into
experiment/scheduled-recalc-workflowfrom
experiment/scheduled-recalc-schedules

Conversation

@rodrigoi

@rodrigoi rodrigoi commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Problem

The coordinator workflow from #108100 is registered but nothing fires it.

Changes

This PR creates the 24 schedules and turns the feature on for teams whose organization has the flag enabled.

One hourly schedule

A single schedule, experiment-scheduled-recalculation, with cron 30 * * * * and no workflow input. Discovery resolves the hour from the clock and selects the teams configured for it, so one schedule serves all 24 hours.

The :30 offset is the coexistence design. The daily timeseries workflow fires at :00 for the same teams, reading the same experiment_recalculation_time config, so the offset gives its sync publish a head start before a real run supersedes it.

The timeseries workflows keep 24 schedules because each runs its own metric queries and can outlive its hour. This coordinator starts other workflows and returns, so a SKIP overlap policy on one schedule covers the same ground.

Creating the schedule also deletes the 24 per-hour schedules this replaces. Temporal keeps a schedule until it is deleted, so leaving them would start the workflow 25 times an hour.

  • products/experiments/backend/temporal/schedule.py
  • products/experiments/backend/temporal/test_scheduled_recalculation_schedule.py
  • posthog/temporal/schedule.py

Documentation

The experiments temporal README documents the daily timeseries workflow and the approximations in its sync row. A new section explains how scheduled recalculations relate to it, including why the freshness check ignores sync rows.

  • posthog/temporal/experiments/README.md

Rollback plan

Turn the organization feature flag off. Discovery returns no candidates, so the schedules keep firing and do nothing. This is the fast lever and it needs no deploy.

Reverting this PR stops the schedules being created or updated, but the 24 already registered in Temporal keep firing, so a revert alone is not enough. To remove them, run delete_experiment_scheduled_recalculation_schedules against the namespace, the same way the sibling timeseries deleter is run.

How did you test this code?

pnpm turbo run backend:test --filter=@posthog/products-experiments
uv run mypy --cache-fine-grained .

Beyond the suite, the creator was run against a local Temporal: it registers one schedule firing at minute 30 of every hour, with no input args, and deletes all 24 of the per-hour schedules it replaces. A wrong cron or a mistyped workflow name fails only in production, since nothing local fires these schedules.

cat-type-small

Release status

  • This change is behind a feature flag and is not available to users

Automatic notifications

  • Publish to changelog?

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Model: Opus 5 (1M context)
Manually refactored: no

Skills used:

  • /brainstorming (superpowers)
  • /writing-plans (superpowers)
  • /subagent-driven-development (superpowers)
  • /writing-tests (local)
  • /writing-pull-requests (local)
  • /stacking-prs (local)

Relevant decisions:

  • Scheduled runs carry the scheduled trigger, which feat(experiments): add missing metric recalculation triggers #107119 added for this workflow and which already advances the recalculation window so every metric recomputes on fresh data.
  • The schedule fires at :30 rather than :00, so the daily timeseries sync publish lands before a real recalculation supersedes it, rather than the two racing for the same teams.
  • One hourly schedule replaced 24 per-hour ones. The hour was only ever workflow input; moving that read into discovery removes 23 objects whose sole difference was a parameter.
  • The plural TeamExperimentsConfig.experiment_recalculation_times field is deliberately not read. Both systems stay on the singular field so a team has one hour for both.

CodeRabbit CLI pass skipped: the CLI is installed but signed out, and authorizing it for the workspace was not available in this session.

@rodrigoi rodrigoi self-assigned this Sep 29, 2026
@rodrigoi
rodrigoi added this pull request to stack #108102 September 29, 2026 02:55
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

⚠️ Comment density — 6% of added code lines are comments (4 of 71)

This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.

Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.

Files with the most added comment lines:

File Comment lines Added lines
products/experiments/backend/temporal/test_scheduled_recalculation_schedule.py 4 39

This check does not block merging. It updates on every push and clears when the share drops.

✅ Django migration risk — no migrations to analyze

No Django migrations need risk analysis.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[High risk] Adds hourly scheduled experiment recalculation workflows.

The PR appears safe to merge, though the scheduled-recalculation documentation should be corrected for teams with two configured hours.

Reviews (2) · Last reviewed commit: "fix(experiments): expose the scheduled r..."

Comment thread products/experiments/backend/temporal/schedule.py Outdated
Comment thread products/experiments/backend/temporal/schedule.py Outdated
Comment thread products/experiments/backend/temporal/test_scheduled_recalculation_schedule.py Outdated
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from 69564ce to c1ac571 Compare September 29, 2026 03:06
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 5a6e4248-ee1b-4f2d-b0ee-5b519a5adc16

📥 Commits

Reviewing files that changed from the base of the PR and between ffafbf4 and 9d52c88.

📒 Files selected for processing (1)
  • tach.toml

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.


📝 Walkthrough

Walkthrough

The change adds 24 daily Temporal schedules that start experiment recalculation workflows at minute 30. It registers schedule creation with general schedule initialization. The schedule function updates existing schedules and attempts to delete each hourly schedule. The README documents eligibility rules, skipped experiments, and coordinator events for started and skipped recalculations.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 9d52c

A failed schedule deletion during rollback can leave hourly coordinator runs in place without reporting the cleanup failure. The organization flag limits recalculation work while disabled, so this is a bounded operational risk rather than a broad release blocker.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 9d52c

Automatic recalculation remains limited by organization eligibility and existing tenant-scoped execution safeguards. The main concern is rollback verification: schedule removal can silently fail, and disabling the feature does not cancel work already selected.

Retained concerns

  • Low · reliability · observed: The new cleanup helper treats all deletion failures as harmless missing schedules and returns without reporting incomplete cleanup. Because registered schedules survive a code revert, an operator can finish the removal procedure while schedules remain active. Disabling the organization flag limits subsequent tenant work, but does not verify schedule removal or cancel previously selected work.
Security review details

Security Blast Radius

  • inferred — Registration affects the connected scheduling namespace and can initiate work across all eligible, flag-enabled organizations. Data work is partitioned by experiment ownership rather than by one global user-supplied tenant identifier. Production rollout breadth is not established.

Trust Boundaries and Controls

  • observed — Discovery requires a non-deleted, running experiment within the age window and its configured hour, then evaluates the organization/project flag. The start path re-fetches the experiment, uses team-scoped recalculation checks, and dispatches with its current ownership.

Resilience and Maintainability Implications

  • observed — SKIP applies within each schedule, not across independent schedules or manual starts. The shared request path adds experiment-row locking and active-run reuse. Dispatch failure can fail an unstarted pending row; stale replacement fails old rows before replacement, and first-write-wins progress updates reject later writes to terminal runs. These controls, rather than the minute-30 offset alone, contain overlapping state transitions.

Hardening Proposals

  • proposed — Make cleanup ignore only confirmed missing schedules, report other failures, and verify that all 24 IDs are absent before declaring removal complete. Document flag disablement as stopping future discovery rather than cancelling in-flight work.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is mostly complete. It includes the problem, user-visible changes, rollback plan, testing, release status, and agent context. It omits the Docs update section and several agent-gate de…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@trunk-io

trunk-io Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
products/experiments/backend/temporal/schedule.py-118-122 (1)

118-122: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Log per-schedule deletion failures instead of swallowing them silently.

except Exception: pass hides real failures such as permission errors or connection loss. It treats them like a missing schedule. A persistent failure leaves schedules running with no signal. Catch the not-found case explicitly and log anything else. The loop keeps going, so other hours still get deleted.

Proposed fix
+    from temporalio.service import RPCError, RPCStatusCode  # noqa: PLC0415
+
     for hour in range(24):
+        schedule_id = f"{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}"
         try:
-            await a_delete_schedule(client, f"{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}")
-        except Exception:
-            pass  # Schedule might not exist
+            await a_delete_schedule(client, schedule_id)
+        except RPCError as e:
+            if e.status != RPCStatusCode.NOT_FOUND:
+                logger.exception("experiment_scheduled_recalculation_schedule_delete_failed", schedule_id=schedule_id)

logger must be defined in this module if it is not already.

Source: Learnings

products/experiments/backend/temporal/schedule.py-94-111 (1)

94-111: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Continue hourly setup, then re-raise the failure.

A failed a_schedule_exists, a_create_schedule, or a_update_schedule call stops later hours. The exception also propagates through the general scheduler’s TaskGroup, which can cancel other schedule initializers.

Catch and log each hourly failure, continue through all 24 hours, then re-raise an aggregated failure. This lets the management command report failure instead of silently succeeding with missing schedules. The repository does not show an in-code retry, so re-raising preserves the failure signal for any external deployment retry.

Suggested fix
+import structlog
+
...
+logger = structlog.get_logger(__name__)
+
...
 async def create_experiment_scheduled_recalculation_schedules(client: Client) -> None:
+    failures: list[Exception] = []
+
     for hour in range(24):
-        schedule_id = f"{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}"
-
-        schedule = Schedule(
-            action=ScheduleActionStartWorkflow(
-                SCHEDULED_RECALCULATION_WORKFLOW_NAME,
-                ScheduledRecalculationWorkflowInputs(hour=hour),
-                id=f'{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}-{{{{.ScheduledTime.Format "2006-01-02"}}}}',
-                task_queue=settings.GENERAL_PURPOSE_TASK_QUEUE,
-            ),
-            spec=ScheduleSpec(cron_expressions=[f"30 {hour} * * *"]),
-            policy=SchedulePolicy(overlap=ScheduleOverlapPolicy.SKIP),
-        )
-
-        if await a_schedule_exists(client, schedule_id):
-            await a_update_schedule(client, schedule_id, schedule)
-        else:
-            await a_create_schedule(client, schedule_id, schedule, trigger_immediately=False)
+        try:
+            schedule_id = f"{SCHEDULED_RECALCULATION_SCHEDULE_ID_PREFIX}-{hour:02d}"
+            ...
+        except Exception as exc:
+            logger.exception("Failed to initialize hourly experiment schedule", hour=hour)
+            failures.append(exc)
+
+    if failures:
+        raise ExceptionGroup("Failed to initialize hourly experiment schedules", failures)

The sequential-latency concern is not independently actionable. Do not parallelize these RPCs without a latency requirement and a plan for partial failures.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: c3555bf3-ec01-4dd6-bd8a-38d5f549963e

📥 Commits

Reviewing files that changed from the base of the PR and between 0634bd6 and c1ac571.

📒 Files selected for processing (4)
  • posthog/temporal/experiments/README.md
  • posthog/temporal/schedule.py
  • products/experiments/backend/temporal/schedule.py
  • products/experiments/backend/temporal/test_scheduled_recalculation_schedule.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.

@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from c1ac571 to 563bf70 Compare September 29, 2026 03:14
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from 6e24cd6 to e169684 Compare September 29, 2026 03:34
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch 2 times, most recently from 05c6e0e to 6eb0ef0 Compare October 1, 2026 05:08
@rodrigoi
rodrigoi marked this pull request as ready for review October 1, 2026 05:10
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team October 1, 2026 05:11
@pr-assigner-resolver-posthog

Copy link
Copy Markdown

👀 Auto-assigned reviewers

These soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:

  • @PostHog/team-devex (owners.yaml)

Soft owners come from each directory's owners.yaml and each product's product.yaml (resolved nearest-file-wins). For a skipped owner, the locator is the file that decided it. Generated files and lockfiles are ignored when deciding ownership.

Comment thread posthog/temporal/experiments/README.md Outdated
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from 6eb0ef0 to 3039c19 Compare October 1, 2026 15:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
products/experiments/backend/temporal/schedule.py-119-120 (1)

119-120: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Propagate non-missing schedule deletion failures.

If handle.delete() raises a non-NOT_FOUND Temporal error, the broad handler returns success while the schedule remains. The feature flag can stop recalculation candidates independently, so this is a cleanup and observability issue rather than a major recalculation failure. Catch only a NOT_FOUND RPCError and propagate other failures.

Suggested fix
+from temporalio.service import RPCError, RPCStatusCode
+
...
-        except Exception:
-            pass  # Schedule might not exist
+        except RPCError as error:
+            if error.status != RPCStatusCode.NOT_FOUND:
+                raise

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 12c040a2-679c-4924-8010-cc88eb4cc0af

📥 Commits

Reviewing files that changed from the base of the PR and between c1ac571 and 3039c19.

📒 Files selected for processing (5)
  • posthog/temporal/experiments/README.md
  • posthog/temporal/schedule.py
  • products/experiments/backend/temporal/schedule.py
  • products/experiments/backend/temporal/test_scheduled_recalculation_schedule.py
  • tach.toml

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch 2 times, most recently from 3ec85ed to ffafbf4 Compare October 1, 2026 16:30
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from ffafbf4 to 9d52c88 Compare October 1, 2026 21:55
@parameterai

parameterai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Risk: No findings

This increment adds only tach.toml architecture-lint declarations: products.webmcp is registered as a module and added to an existing dependency allowlist, and products.ai_observability interface exposures are declared (including a documented set of legacy leaks). These are static import-lint rules with no runtime, auth, or data-path effect; nothing security-relevant is introduced.

Sentinel reviewed 832ca2c · Review settings

@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch 2 times, most recently from bbb3653 to 00d736b Compare October 2, 2026 03:14
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from 00d736b to b42cf82 Compare October 2, 2026 04:19
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch 2 times, most recently from 4998846 to ed2fd71 Compare October 2, 2026 04:30
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from ed2fd71 to 59815f5 Compare October 2, 2026 05:15
@trunk-io

trunk-io Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

✨ Stack submitted to Merge by Rodrigo Iloro (a GitHub user). It will be added to the merge queue once all branch protection rules pass. See more details here.

@gantoine

gantoine commented Oct 2, 2026

Copy link
Copy Markdown
Member

Heads up, this stack is failing in the merge queue (Django Tests Pass) and will be dropped once the PRs ahead of it finish.

#110127 merged this morning and removed TeamExperimentsConfig.experiment_recalculation_time. The new tests in products/experiments/backend/temporal/test_scheduled_recalculation_logic.py still set it (lines 87, 101 and 112), so three tests fail with:

FieldError: Invalid field name(s) for model TeamExperimentsConfig: 'experiment_recalculation_time'

To fix it, rebase on master, switch those tests to experiment_recalculation_times only (including the defaults={...} on line 112), then resubmit the stack from this PR. A plain requeue will fail the same way.

@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from 7626970 to 832ca2c Compare October 2, 2026 13:42
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from 832ca2c to 8b18027 Compare October 2, 2026 13:47
@rodrigoi
rodrigoi force-pushed the experiment/scheduled-recalc-schedules branch from 8b18027 to 9c2c13c Compare October 2, 2026 15:16

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants