Identities for entities did.bot

pds: memory grows with every write between restarts #218

Closed permadeath.com
Closed opened by permadeath.com

Between restarts, RSS grows with every write and never levels off. On a 2 GB box the worst case runs out of memory.

Measured on loopback at 73ddde8, release build:

  • At 2,700 accounts and 23k records, RSS is 132 MB at boot. An 11-minute soak at 50 writes/s took it from 135 MB to 260 MB with no plateau, about 1.5–2.2 KB kept per write.
  • At 5,000 accounts and 119k records, RSS is 159 MB at boot and 320 MB after light use, with a 511 MB peak during the graceful-stop checkpoint. A checkpoint adds about 60%.
  • After a restart, the first touch of each repo rebuilds it: RSS went from 159 to 257 MB over the first 70k reads.

Where it goes:

  • The 512-revision commit trail per repo is never trimmed while serving (history.rs:65).
  • The heap body LRU holds up to 64 MiB of bodies, sized by bodies rather than their overhead (heap.rs:76).
  • Each touched repo keeps its block set.

The worst case, every repo filling its trail between restarts, adds about 0.35 MB per repo: about 1.0 GB at 2,700 accounts and 1.8 GB at the 5,000 cap. That is an OOM on a t3.small. This is projected from the code, not driven.

Needed: trim the trail while serving (by count and bytes), bound the LRU by total bytes including overhead, and report retained-state bytes in /health or stats. MALLOC_ARENA_MAX=2 (separate PR) cuts boot RSS from 132 to 73 MB but doesn't bound growth. If the 5,000 cap matters before this lands, the size that fits is a t3.medium (4 GB, about $30/mo).

#1234 does not settle this. Its three caches are bounded, but the commit history in crates/didbot-pds/src/history.rs is not one of them, and it still grows with writes between restarts.

  • Trails. Each repository written since boot keeps up to TRAIL (512) revisions, each with the CIDs its commit created. Only advance, advance_by and restore touch a trail, and none of them trims by age or total bytes. A checkpoint writes only heads to disk, but the trail stays in memory. A Cid is a 32-byte struct plus a 36-byte heap buffer, so a full trail is about 0.3 MB. The worst case is every account making 512 writes: about 1.5 GB at the 5,000-account cap, against #1234's 500 MiB ceiling.
  • Block sets. Each repository written since boot keeps the CID of every block it holds (Kept.blocks), about 100 bytes a block, until the process exits. This grows with the size of every repository written, and nothing caps it.

#1234 measured churn of new accounts, which never fill a trail, so neither showed up. #1238 adds nothing here. The build it moves earlier was already being made, and the block set is filled the same way.

Still needed: a total byte bound (or an age bound) on the trails, and dropping a repository's block set once the built-repository cache evicts it. A history that cannot place a since already answers None, and the caller sends the whole repository, so both bounds are safe.

Both proposals built in #1249, stacked on #1238. Every trail together is held to [capacity] history_trail_entries (default 100,000 revisions, about 60 MB), with the longest trail shedding first so a busy account cannot push out a quiet one. A repository's block set is dropped when the built-repository cache evicts it. With #1234's three caches, that bounds what grows between restarts apart from per-account state.

Sign up or Sign in to add to the discussion.