Skip to content

Arm deadline warnings per phase and delete the per-minute sweep - #1391

Merged
johnpooch merged 2 commits into
mainfrom
claude/modest-mccarthy-qvqthc
Sep 24, 2026
Merged

johnpooch merged 2 commits into
mainfrom
claude/modest-mccarthy-qvqthc

Conversation

@johnpooch

@johnpooch johnpooch commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What this PR does

Closes #1073 and supersedes the stale #1097. This is a fresh reimplementation on current main.

It deletes the phase.send_deadline_warnings cron job, which ran every minute and scanned every active game with a heavily prefetched queryset. In its place, each phase gets its own phase.send_deadline_warning(phase_id) job, scheduled at scheduled_resolution - deadline_warning_offset(...). This works the same way phase resolution is already scheduled.

Scheduling the job

  • Add Phase.warning_job_id and PhaseManager.arm_warning. arm_warning has the same structure as arm_resolution: it reads the current job, computes the schedule, and if the schedule changed it cancels the old job and defers a new one. It is called from:
    • a second post_save receiver, next to the resolution one;
    • _apply_nmr_extensions, because that path moves the deadline with .update(), which does not fire post_save;
    • Game.pause / Game.unpause. unpause saves the phase while the game is still paused, so without this the warning would be cancelled and never re-armed.
  • Keep a live job instead of re-arming it. An existing job is kept when its scheduled_at still matches and its status is todo, doing or succeeded. So a warning that has already gone out is not armed again when the phase is saved with the same deadline. A failed or cancelled job is replaced.

Sending the warning

send_deadline_warning runs in one transaction with the phase row locked. It only sends when all of these hold:

  • the phase is armable (filter_armable());
  • the deadline is still in the future;
  • the warning time for the current deadline has arrived. This catches a job that was already running when the deadline moved and so could not be cancelled.

It keeps the old per-player checks (has_possible_orders, forced orders, actionable units, not confirmed, has a user). It also skips any player already warned for this exact deadline, using deadline_warning_sent_for. Because of that check and the transaction, a retry, a duplicate run or two jobs racing cannot warn anyone twice, and a failed send leaves no notifications or markers behind.

It emits deadline_warning exactly as before; the notification copy is unchanged.

Other changes

  • Threshold table: moved into the pure helper phase.utils.deadline_warning_offset.
  • Deleted: the periodic task and the send_deadline_warnings manager method.

deadline_warning_sent_for is kept (differs from #1097)

#1364 merged after #1097 and gave this field a second job: when a player was warned at an earlier deadline on the same phase, their next warning starts with "The deadline has been extended - still …". So the field stays. It now does two things:

  • chooses between the normal and "extended" wording;
  • stops a second warning for the same deadline.

Backfill in the migration (beyond #1097)

Migration 0023 adds the field and then arms a warning job for every live phase, following 0021_arm_resolution_jobs. Phases already inside their warning window are armed too, so their job runs immediately. The per-deadline marker skips players the old sweep already warned, and there is no gap between the last sweep and the migration.

Tests

  • test_background_resolution.py::TestDeadlineWarningArming covers:
    • arming on create at deadline - offset;
    • no job for a pending phase, or one with no deadline;
    • cancel and re-arm when the deadline changes by save, extend_deadline or NMR extension;
    • no re-arm after the warning has fired for an unchanged deadline;
    • a failed job is re-armed on the next save;
    • pause cancels the job, and unpause re-arms it at the extended deadline;
    • cancel on completion;
    • a job running before its warning time sends nothing;
    • running the armed job warns only the unconfirmed player.
  • test_deadline_reminders.py covers the fire-time checks:
    • completed, processing, past-deadline, paused and missing phases are skipped;
    • a player already warned for this deadline is not warned again;
    • a failed send leaves no notification or marker behind.
  • phase/tests.py: the body-copy tests now call send_deadline_warning(phase.id), including both Stop deadline warnings piling up and repeating themselves #1364 "extended" tests.
  • test_utils.py: a parametrised test for deadline_warning_offset.
  • Query counts: assertions in phase and game went up by 1–5 queries per phase save, all from arming the warning. I checked this against the logged queries.
  • Mutation check: I reverted each fix in the second commit one at a time and confirmed the matching new test fails.

Full backend suite: 2556 passed, 8 skipped, 1 failed. The failure is harness/tests.py::TestQualityMetricAggregation::test_accuracy_covers_ranked_samples_and_skips_the_rest, which also fails on main without this change.

Checklist

  • This PR does one thing — no unrelated fixes, refactors, or drive-by cleanups bundled in
  • For PRs of any significant complexity: I ran /review-pr against this PR in Claude Code and addressed (or responded to) its findings
  • Tests cover the change
  • Screenshots embedded in the PR description for any visual changes (see CLAUDE.md) — N/A, backend-only

Note: this makes 8 open PRs, over the soft limit of 5.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRFZ6aT4agJnpbA6WXJJVc

The phase.send_deadline_warnings cron ran every minute over every active
game with a heavily prefetched queryset. Replace it with a
phase.send_deadline_warning job armed at scheduled_resolution minus the
warning offset, mirroring the resolution arming. The phase post_save
receiver and the NMR extension path re-arm it when the deadline or status
changes. A job that already fired for an unchanged deadline is left
alone, so a same-deadline save does not warn twice.

Recipients are evaluated when the job fires, so the per-player
deadline_warning_sent_for dedup check goes away. The field stays: the
fire-time method still writes it and reads it to choose the "deadline has
been extended" copy from #1364.

The warning-threshold table moves into phase.utils.deadline_warning_offset.
Migration 0023 adds Phase.warning_job_id and arms a warning for every live
phase whose warning time is still ahead, so phases already running at
deploy keep their warning.

Closes #1073. Supersedes #1097.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRFZ6aT4agJnpbA6WXJJVc
@github-actions

Copy link
Copy Markdown
Contributor

Warning

WIP limit exceeded. There are now 8 open pull requests — the project target is 5 or fewer.

Please close or merge an existing PR before continuing with this one.

Unpausing saved the phase while the game was still paused, so arm_warning
cancelled the job and nothing re-armed it. pause and unpause now re-arm
the warning alongside the resolution.

A warning job already running when the deadline moved could not be
cancelled and sent early against the new deadline. The fire-time method
now also requires the warning time for the current deadline to have
arrived.

arm_warning kept any job whose schedule matched, including failed and
cancelled ones. It now keeps only todo, doing and succeeded jobs.

The send now runs in one transaction with the phase row locked, and it
again skips a player already warned for this exact deadline. A retry,
a duplicate run or a racing job cannot warn anyone twice, and a failed
send leaves no notifications or markers behind.

The backfill migration arms every live phase, including those already
inside their warning window. The per-deadline marker keeps players the
old sweep already warned from being warned again, which closes the gap
between the last sweep and the migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRFZ6aT4agJnpbA6WXJJVc
@johnpooch
johnpooch merged commit 4a7f7b1 into main Sep 24, 2026
22 checks passed
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.

Notifications PR 5: signal-arm the deadline warning and delete the sweep

2 participants