Relationships
#3468 serve and swamp dashboard shutdown waits seconds for the run index warm-up: the plain index parse is not abortable, and the warm-up waits on request-led loads it joined (regression from #3105)
Opened by hammz · 10/14/2026· Shipped 10/14/2026
Summary
Since #3105 (c5038e5b, swamp-club#3443), the run index warm-up that swamp serve and the swamp dashboard child start once listening (RunIndexWarmup, src/serve/run_index_warmup.ts) can delay shutdown by seconds, scaling with index size. Before #3105, SIGTERM exited in about 0.05 s in the same situations. A second SIGTERM does not force an exit. Exit codes and output are otherwise unchanged, and no index is ever left partial.
1. The warm-up's plain index read is not abortable
The warm-up passes an AbortSignal, but it is only checked between records during a rebuild or repair (rebuildIndexUnqueued, findSuspectRecords, reconcileSuspects). The common path — listDirEntries + readRunIndex (JSON.parse) + indexToSummaries + RunIndexSnapshot.of — runs to completion, and shutdown awaits it (runIndexWarmup.finished() after the drain).
Measured on a repo with one workflow of 300k runs (94 MB .runs-index.json), SIGTERM 0.1–1 s after "listening":
| Exit after SIGTERM | |
|---|---|
| post-#3105 serve | 1.1 s, 1.1 s, 1.7 s, 1.8 s, 2.1 s, 3.3 s, 8.6 s (load-dependent) |
| 3999f85e serve | 0.05 s |
post-#3105 swamp dashboard --child, stdin closed |
~3.4 s |
| 3999f85e dashboard child | 0.02 s |
That workflow has more runs than the cache's 250,000-run cap (RUN_INDEX_CACHE_MAX_RUNS), so its snapshot is never kept: the 2–10 s of warm-up spent on it is wasted on every serve start.
2. Shutdown waits for a request-led load the warm-up joined
When a request starts a workflow's snapshot load and the warm-up reaches the same workflow, the warm-up waits on the shared in-flight load (RunIndexCache.get) without watching its own signal. finished() therefore waits for the request's whole load. Repro: a 50k-run workflow whose index is deleted right after "listening", a workflow.run.summary sent at once (it leads the rebuild), then SIGTERM 2.5 s later. Serve exits 2.64 s and 2.75 s after the signal (the request was answered correctly, and the rebuild wrote the index during shutdown). OLD exits in 0.04 s. For a large rebuild this would be tens of seconds.
Suggested fix
- Have the warm-up race its
warm()promise against its own abort signal, sofinished()resolves promptly on stop even while a shared or non-abortable load continues (or is abandoned). - Skip warming any workflow whose index would exceed the cache cap (its run count is known after listing, or from the previous load), since it is never kept.
- Optionally check the signal between the list, parse and snapshot phases.
Environment
Linux x86_64, Deno 2.9.7, swamp at 37627d76 ≡ c5038e5b. Found during adversarial e2e verification of #3105; OLD = 3999f85e.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.