Skip to content

fix: eliminate book-slug cache stampede that OOM-crashed search (v5.2.0)#13

Merged
ttpears merged 1 commit into
mainfrom
fix/search-slug-stampede-oom
Jul 17, 2026
Merged

fix: eliminate book-slug cache stampede that OOM-crashed search (v5.2.0)#13
ttpears merged 1 commit into
mainfrom
fix/search-slug-stampede-oom

Conversation

@ttpears

@ttpears ttpears commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Problem

The deployed connector was OOM-crashing. Swarm logs showed hundreds of concurrent BookStack rate limited (429); retry N/5 in 44000ms followed by FATAL ERROR: Reached heap limit — JavaScript heap out of memory.

Root cause (cache stampede cascade): Enhancing search / recent-changes / list results resolves a book slug per item via getBookSlug. The cache check-then-set is not atomic across the Promise.all batch, so results sharing a book all miss the cache at once and fan out one concurrent GET /books/{id} per result (reproduced: 50 same-book results → 50 requests). BookStack 429s the burst; each retry holds buffers for tens of seconds in the manual backoff, the pending promises pile up, and the heap exhausts → container crash. Compounded by no HTTP timeout on the axios client, so a slow/hung /search never returns.

Fix

  • getBookSlug bulk-warms the slug cache from a single /books listing (memoized on first use), turning the per-result N+1 into +1. Gaps (books created after the warm) fall back to an in-flight-deduped lazy fetch so a batch can't stampede.
  • axios now has a configurable timeout (default 30s, BOOKSTACK_TIMEOUT_MS) so a hung request aborts cleanly.

Verification

Scenario Before After
50 same-book results 50 book lookups 1
50 results / 30 books ~30–50 1
40 results, gap book 40 2 (1 warm + 1 deduped)
Hung request hangs forever aborts (ECONNABORTED)

npm run type-check and npm run build clean. Bumps version to 5.2.0.

🤖 Generated with Claude Code

Enhancing search/recent/list results resolved a book slug per item via
getBookSlug. The cache check-then-set was not atomic across the Promise.all
batch, so results sharing a book all missed the cache simultaneously and
fanned out one concurrent GET /books/{id} per result. On large result sets
BookStack rate-limited the burst (429); each retry held buffers for tens of
seconds and the pending promises accumulated until the Node heap OOM-crashed
the container. There was also no HTTP timeout, so a slow/hung BookStack
response never returned.

- getBookSlug now bulk-warms the slug cache from a single /books listing
  (memoized), turning the per-result N+1 into +1; gaps fall back to an
  in-flight-deduped lazy fetch so a batch cannot stampede.
- axios gets a configurable timeout (default 30s, BOOKSTACK_TIMEOUT_MS) so a
  hung request aborts cleanly instead of hanging until the client gives up.

Bumps version to 5.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ttpears
ttpears merged commit 413ae3d into main Jul 17, 2026
3 checks passed
@ttpears
ttpears deleted the fix/search-slug-stampede-oom branch July 17, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant