send-admin-emails-via-a-background-job-with-live-status-reporting-462 #476
No reviewers
Labels
No labels
Kestra
bug
enhancement
someday
subtask
☁️ api
🎛️ infrastructure
🐞 sentry
📆 2025 Season
📝 pages
allpicks
📝 pages
picks
📝 pages
standings
🚀 performance
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
johnsturgeon/tgfp-web!476
Loading…
Reference in a new issue
No description provided.
Delete branch "send-admin-emails-via-a-background-job-with-live-status-reporting-462"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Send admin emails via a background job with live status
Enqueue one EmailQueue row per recipient and drain the batch off-request
with a FastAPI background task. The landing page polls a status endpoint
and flips a light per recipient as each send lands, replacing the inline
synchronous send. Idempotent (already-SENT rows are skipped) and scoped
per batch so concurrent sends never collide.
resolves #462
Summary
This cleanly moves the admin email send off-request: it enqueues one
EmailQueuerow per recipient, drains them in a FastAPI background task, and the landing page polls a per-recipient status endpoint. The design is sound, the tests are thorough, and the unrelatedteam.data_sourcedrop is verified dead (no references remain inapp/). No blocking issues.Worth fixing
A crash mid-drain strands rows unrecoverably (
app/jobs/send_admin_email.py:13).drain_batchmarks a rowPROCESSINGand commits before the send, but the only way a batch is ever drained is the in-processbackground.add_taskfired once at request time. If the worker dies during a send (deploy, OOM, restart), that row staysPROCESSINGand any still-PENDINGrows stayPENDINGforever — nothing re-triggers the batch. Re-invoking the drain wouldn't help either, sincejobs_in_batch(..., EmailSendStatus.PENDING)filters outPROCESSINGrows. The PR frames this as a "background job" and stresses idempotency, but idempotency only covers the already-SENTcase. Since the codebase already runs APScheduler, a small startup/periodic sweep that re-queues stalePENDING/PROCESSINGbatches would close the durability gap. At minimum, worth documenting this as a known limitation indocs/TECH_DEBT.md.donenever flips true for a stranded batch (app/routers/v2.py:634). Consequence of the above: with a stuckPROCESSINGrow the status endpoint'sdonestaysFalse, so the poller just runs out itsMAX_TICKSand shows "Still sending — reload to refresh." That's a reasonable fallback, but the admin has no signal distinguishing "genuinely slow" from "permanently stuck."Nits
Unrelated migration bundled in (
alembic/versions/a390332fa722_drop_team_data_source.py). Droppingteam.data_sourceis harmless and well-documented, but it's scope creep in a PR titled "send admin emails via a background job." Ordering it after the email-queue migration also couples the two revisions; a reviewer bisecting the email feature can't revert it independently.Missing type hint (
app/jobs/send_admin_email.py:10).async def drain_batch(batch_id):— the surrounding code annotates params (e.g.jobs_in_batch(session, batch_id: str, ...)). Addbatch_id: str.Prune coupled to the send path (
app/routers/v2.py:605-606). Running a 30-dayDELETEinside every send request's transaction works, but it ties table housekeeping to a user action; a scheduled prune would be a more natural home and keeps the request lean. Also notecreated_atis naive and this compares against a naive-UTC cutoff, which relies on the DB server clock being UTC — consistent with the existingTGFPModelBaseconvention, so only flagging for awareness.Test gaps for the new edges (
tests/test_send_admin_email_job.py). The drain tests are good, but nothing exercises the 30-day prune or aPROCESSINGrow left behind by a crash — the two behaviors most likely to surprise later. Worth a row for each.