Skip to content

fix(seer): Deduplicate night shift cron runs#120609

Draft
trevor-e wants to merge 5 commits into
masterfrom
feat/night-shift-run-idempotency
Draft

fix(seer): Deduplicate night shift cron runs#120609
trevor-e wants to merge 5 commits into
masterfrom
feat/night-shift-run-idempotency

Conversation

@trevor-e

@trevor-e trevor-e commented Jul 24, 2026

Copy link
Copy Markdown
Member

Cron-task redelivery could recreate and execute Night Shift runs for organizations already covered by the same scheduler window. Cron dispatches now carry an identifier derived from the deployed beat crontab; a partial unique constraint creates one durable run per (organization, workflow configuration, schedule window).

Each run persists its complete shard plan before creating feature runs. Redelivery resumes only planned shards that have not yet been linked to a SeerRun, while completed windows no-op; this prevents both gaps after interruption and duplicate feature-run dispatches. The new nullable fields leave manual and historical runs unchanged, so no backfill is required. Refs AIML-3137.

Make cron-triggered runs idempotent per organization, workflow configuration, and schedule window so task redelivery cannot repeat triage work. Manual runs remain independent.

Refs AIML-3137
@linear-code

linear-code Bot commented Jul 24, 2026

Copy link
Copy Markdown

AIML-3137

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jul 24, 2026
Comment thread src/sentry/tasks/seer/night_shift/cron.py Outdated
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

This PR has a migration; here is the generated SQL for src/sentry/seer/migrations/0030_add_night_shift_run_schedule_id.py

for 0030_add_night_shift_run_schedule_id in seer

--
-- Add field schedule_id to seernightshiftrun
--
ALTER TABLE "seer_nightshiftrun" ADD COLUMN "schedule_id" varchar(256) NULL;
--
-- Add field date_completed to seernightshiftrun
--
ALTER TABLE "seer_nightshiftrun" ADD COLUMN "date_completed" timestamp with time zone NULL;
--
-- Create constraint seer_nightshiftrun_unique_org_config_schedule on model seernightshiftrun
--
CREATE UNIQUE INDEX CONCURRENTLY "seer_nightshiftrun_unique_org_config_schedule" ON "seer_nightshiftrun" ("organization_id", "workflow_config_id", "schedule_id") WHERE "schedule_id" IS NOT NULL;

trevor-e added 2 commits July 24, 2026 18:58
Use the standard bounded text capacity while documenting the current cron-derived schedule window format.
Persist shard plans before dispatching feature runs so redelivered schedule tasks resume only missing shards without repeating existing work.
Comment thread src/sentry/tasks/seer/night_shift/cron.py Outdated
Comment thread src/sentry/tasks/seer/night_shift/cron.py
trevor-e added 2 commits July 24, 2026 19:48
Leave quota, eligibility, access, and shard dispatch failures resumable, and clear recovered errors when a run completes.
Refetch state after recovery so backend typing does not retain stale nullability assumptions.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f73d70b. Configure here.


if not eligible:
logger.info("night_shift.no_eligible_projects", extra=log_extra)
_complete_run(run)

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.

Stale extras clobber completed runs

Medium Severity

Deduped cron redelivery can run concurrent workers on the same SeerNightShiftRun. Those workers still do unlocked read-modify-write run.update(extras=...) for num_eligible_projects / num_candidates from a stale in-memory extras snapshot. If another worker finishes first and _complete_run clears error_message and sets date_completed, a later stale extras write can put error_message back. A crash before that worker’s own _complete_run leaves a completed window permanently showing an error, because later redeliveries no-op on date_completed.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f73d70b. Configure here.

Comment on lines +285 to +290
run, created = SeerNightShiftRun.objects.get_or_create(
organization=organization,
workflow_config=workflow_config,
schedule_id=schedule_id,
defaults={"extras": extras},
)

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.

get_or_create on partial unique constraint without IntegrityError handling

Concurrent cron redeliveries for the same (organization, workflow_config, schedule_id) can race inside get_or_create and raise IntegrityError, crashing the task. Wrap the call in try/except IntegrityError and fall back to fetching the existing row, or switch to an insert-or-fetch pattern.

Evidence
  • run_night_shift_for_org is an @instrumented_task with processing_deadline_duration=5 * 60; cron-beat redelivery can enqueue duplicate tasks for the same window.
  • The PR description explicitly calls out "Cron-task redelivery could recreate and execute Night Shift runs for organizations already covered by the same scheduler window."
  • The changed code uses SeerNightShiftRun.objects.get_or_create(organization=organization, workflow_config=workflow_config, schedule_id=schedule_id) when schedule_id is not None.
  • Both the migration (0030_add_night_shift_run_schedule_id.py) and the model (SeerNightShiftRun.Meta) enforce a partial unique constraint on (organization, workflow_config, schedule_id) where schedule_id__isnull=False.
  • Django’s get_or_create is not atomic across concurrent transactions for the same unique fields; both callers can miss the .get(), then both .create(), causing IntegrityError on the second insert.
  • The code does not catch IntegrityError around the get_or_create, so the task will crash and likely be retried, matching the production pattern of concurrent get_or_create races documented in the database-integrity reference.

Identified by Warden · sentry-backend-bugs · VYP-T3V

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

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant