Pipeline lifecycle hardening: clear-path row locks, tracked dispatch tasks, shared SSE LISTEN connection #53
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot#53
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
(1)
expire_alerts_job(scheduler.py:55-76) andpoll_alerts_job→_clear_missing_alerts/send_pending_lifted_notificationsboth selectuncleared/unlifted rows in separate sessions with no locking; overlap can
double-send all-clears. Use
with_for_update(skip_locked=True)on thoseselections (or funnel clearing through one job).
(2) Dispatch tasks are fire-and-forget (
alert_processor.py:656-659); theevent loop holds only weak refs, so tasks can be GC-cancelled mid-send and
are not awaited on shutdown. Track them in a module-level set with
done-callbacks; cancel/await in lifespan shutdown.
(3) Each SSE client opens a dedicated asyncpg LISTEN connection
(app/api/sse.py:46-53) with no cap — including the public endpoint. Move to
one shared LISTEN connection with in-process fan-out to bounded per-client
queues.
Acceptance criteria:
Filed from the 2026-07-17 codebase audit (
docs/.internal/report-2026-07-17.md), finding F-14.Done in #100 (merged). (1) Clear/lifted row selections take
with_for_update(skip_locked=True)so overlapping runs can't double-send all-clears. (2) Dispatch is now inline via the outbox (the GC-able fire-and-forget task is gone); the remaining background tasks (e.g. the heartbeat ping) are tracked in a module set and drained on shutdown. (3)app/api/sse.pynow uses a single shared LISTEN broker fanning out to per-client bounded queues (drop-oldest on overflow), started/stopped in the app lifespan — replacing the one-Postgres-connection-per-client model.