fix(server): graceful shutdown with open SSE clients + bounded table growth #5

Merged
pregno merged 2 commits from fix/server-lifecycle-retention into main 2026-06-13 01:27:44 +02:00
Owner

Two Important findings from the repo-wide review:

  1. Graceful shutdown no longer hangs on open SSE connections. Fastify v5's default forceCloseConnections: "idle" only closes idle keep-alive sockets; a connected EventSource kept app.close() pending forever, so SIGTERM never reached process.exit. buildApp() now passes forceCloseConnections: true. Regression test does a real listen + fetch on /api/stream, waits for the connected frame, and asserts app.close() resolves within 2s (fails without the fix).

  2. market_alerts and signal_outcomes no longer grow without bound. The hourly prune (pruneSnapshots → renamed pruneOldRows) now also deletes both tables' rows older than retentionDays (same window as snapshots — summarizeOutcomes full-scans on every /api/outcomes call and the UI contract is recent-200 + summary, so no longer horizon is needed; documented in a comment). listPendingOutcomes(minTs) now takes a required floor — computed in OutcomeTracker.fill as the longest horizon (4h) + 24h grace — so permanently unpriceable symbols stop being re-fetched every minute forever.

Tests: 56 files / 317 passing, typecheck clean.

Two Important findings from the repo-wide review: 1. **Graceful shutdown no longer hangs on open SSE connections.** Fastify v5's default `forceCloseConnections: "idle"` only closes idle keep-alive sockets; a connected EventSource kept `app.close()` pending forever, so SIGTERM never reached `process.exit`. `buildApp()` now passes `forceCloseConnections: true`. Regression test does a real `listen` + fetch on `/api/stream`, waits for the connected frame, and asserts `app.close()` resolves within 2s (fails without the fix). 2. **`market_alerts` and `signal_outcomes` no longer grow without bound.** The hourly prune (`pruneSnapshots` → renamed `pruneOldRows`) now also deletes both tables' rows older than `retentionDays` (same window as snapshots — `summarizeOutcomes` full-scans on every `/api/outcomes` call and the UI contract is recent-200 + summary, so no longer horizon is needed; documented in a comment). `listPendingOutcomes(minTs)` now takes a required floor — computed in `OutcomeTracker.fill` as the longest horizon (4h) + 24h grace — so permanently unpriceable symbols stop being re-fetched every minute forever. Tests: 56 files / 317 passing, typecheck clean.
Fastify v5's default forceCloseConnections "idle" only closes idle
keep-alive sockets; hijacked SSE replies are never idle, so app.close()
hung forever while any EventSource client was connected and SIGTERM
never finished. Pass forceCloseConnections: true so close() destroys
active sockets too.
market_alerts and signal_outcomes grew without bound: only snapshots
were pruned, and summarizeOutcomes full-scans signal_outcomes on every
/api/outcomes call. pruneSnapshots becomes pruneOldRows and deletes
old rows from all three tables using the same retentionDays (outcomes
share the snapshot window — the summary scan stays bounded without
promising more history than the rest of the app keeps).

listPendingOutcomes now takes a minTs floor, derived in the tracker
from the longest horizon (4h) plus 24h grace, so permanently
unpriceable symbols stop being re-fetched on every minutely fill pass
forever.
pregno force-pushed fix/server-lifecycle-retention from b57b3d7e80 to 3b9dbb7dca 2026-06-13 01:27:14 +02:00 Compare
pregno merged commit 782c9aebed into main 2026-06-13 01:27:44 +02:00
pregno deleted branch fix/server-lifecycle-retention 2026-06-13 01:27:44 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pregno/polymarket-screener!5
No description provided.