Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneeshammz

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, so finished() 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.

02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 19 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/14/2026, 8:20:16 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
hammz assigned hammz10/14/2026, 6:47:25 PM

Sign in to post a ripple.