Skip to content

fix: bound registry RSS growth (cap pooled save buffer + return freed pages)#102

Open
TeoSlayer wants to merge 1 commit into
mainfrom
fix/registry-rss-retention
Open

fix: bound registry RSS growth (cap pooled save buffer + return freed pages)#102
TeoSlayer wants to merge 1 commit into
mainfrom
fix/registry-rss-retention

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

Root cause (live heap profile during the recurring registry wedge)

The registry re-wedges ~daily (the outage this morning). A heap profile of the live process showed live Go heap ~4 GB but RSS ~22.7 GB and climbing ~3.3 GB/hr with a stable ~207K connection count — so it's not a connection leak. Two compounding causes:

  1. flushSave serializes the whole snapshot (~2.7 GB at 218K nodes) into a sync.Pool'd bytes.Buffer (bytes.growSlice = 67% of live heap). The pool retains that buffer forever, and it grows with node count.
  2. With GOMEMLIMIT=48 GiB, the runtime keeps freed pages instead of returning them to the OS, so RSS ratchets toward the ceiling and eventually GC-thrashes → wedge.

Fix (no on-disk format change)

  • putSaveBuf drops buffers larger than maxPooledSaveBuf (128 MB) instead of returning them to the pool → the giant snapshot buffer becomes GC-eligible after each save.
  • After a large snapshot, shouldScavenge triggers an async debug.FreeOSMemory() at most once per 3 minutes → returns freed pages to the OS, keeping RSS near the true working set.

Safety: neither change touches the snapshot bytes, checksum, WAL, or load path. Worst case is an extra allocation or GC — never data corruption.

Tests

TestPutSaveBufDropsOversized, TestPutSaveBufKeepsNormal, TestShouldScavengeGate (small-snapshot skip, fires-after-interval, rate-limited-within-window). Existing save/persist/load tests pass; full go test -race green.

Operational note

The live registry (running the #100 perf binary) will hit the 48 GB ceiling and wedge in ~7-8 h without this. A restart resets the clock (~14 h) at the cost of a reconverge dip; deploying this bounds it permanently. Recommend landing with #100 and building a combined binary.

🤖 Generated with Claude Code

…d pages

Root-caused from a live heap profile during the recurring registry wedge:
live Go heap was ~4GB but RSS ~22.7GB and climbing ~3.3GB/hr with a stable
connection count. Two compounding causes, both fixed here without touching
the on-disk snapshot format:

- flushSave encodes the whole snapshot (~2.7GB at 218k nodes) into a
  sync.Pool'd bytes.Buffer that grows with node count and is retained
  forever. putSaveBuf now drops buffers larger than maxPooledSaveBuf (128MB)
  instead of returning them to the pool, so the giant buffer is GC-eligible
  after each save.
- With GOMEMLIMIT=48GiB the runtime holds freed pages rather than returning
  them, so RSS ratchets toward the ceiling and eventually GC-thrashes. After
  a large snapshot, shouldScavenge triggers an async debug.FreeOSMemory at
  most once per 3 minutes to return the freed pages to the OS.

Neither change touches the snapshot bytes, checksum, WAL, or load path — the
worst case is an extra allocation or GC, never data corruption.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

2 participants