You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
FFF's LMDB environments are opened without max_readers and without MDB_NOTLS, so LMDB runs in default TLS mode: a reader slot is pinned per thread and only released when the process exits. In long-lived embedding processes (Neovim, Node-based agents via fff-node, …) that accumulate more than 126 reader threads over days, the reader table fills up and every subsequent env open fails:
FFF init failed: Failed to init frecency db: Failed to start read transaction
for frecency database: MDB_READERS_FULL: Environment maxreaders limit reached
This is the complementary case to #460: clear_stale_readers() (added in #468) reclaims slots of dead PIDs, but in our incident all slot holders were alive — 7 long-lived processes holding 15–19 slots each, 126/126 total:
pid 3118389 19 slots pi (up 5 days)
pid 648283 19 slots pi (up 5 days)
pid 1033540 19 slots pi (up 1 day)
... (7 processes, 126 slots total)
So the fix from #468 cannot help here — nothing is stale.
Environment
@ff-labs/fff-node 0.10.3 (libfff_c.so from @ff-labs/fff-bin-linux-x64-gnu 0.10.3)
Host: Node v22 embedding via the pi coding agent + pi-pretty 0.6.21 (frecency/history at ~/.pi/agent/pi-pretty/fff/)
Linux x86_64 (glibc), kernel threads: Node main + libuv threadpool + fff native threads
Same behavior reproduced with fff-node 0.9.6 before upgrading
Evidence
Reader-table forensics on frecency.mdb/lock.mdb (8 KiB page, 64-byte slots → 126 slots + header, matching the LMDB default):
# lock.mdb page 0: header at +0, slots at 64-byte stride, pid at slot+8importos, structlock=open("frecency.mdb/lock.mdb", "rb").read()
live= {int(d) fordinos.listdir("/proc") ifd.isdigit()}
foroffinrange(64, len(lock) -16, 64):
pid=struct.unpack_from("<I", lock, off+8)[0]
ifpid:
print(pid, "ALIVE"ifpidinliveelse"DEAD")
Controlled probes (throwaway DBs, small tree):
Short-lived process, 10 create/destroy/destroy cycles of FileFinder → exactly 3 slots (main + 2 native threads), stable across cycles — slots are per-thread, not per-finder or per-open. Env pooling from fix(pi-fff): stop reopening main LMDB envs in aux finders (#700) #701 works.
Long-lived agent processes → 15–19 slots each and growing with uptime; nothing frees them until the process exits.
Current code
On main, EnvOpenOptions only sets map_size (+ max_dbs):
No max_readers(...), no NOTLS flag. clear_stale_readers() on open (env_pool.rs) only helps dead PIDs.
Suggestions
Cheap mitigation: raise max_readers explicitly (e.g. 256–512). Reader slots are tiny; the cost is negligible, and it buys embedding hosts a lot of headroom.
Structural fix: open the envs with MDB_NOTLS so read transactions borrow a slot from the pool only for the txn lifetime instead of pinning one per thread. This is the standard recommendation for hosts with transient/many threads (libuv threadpool, LSP-style worker churn). Caveat: in NOTLS mode a read txn must begin and end on the same thread — since fff owns its txn lifecycles inside fff-core, this should be enforceable there.
Optionally expose the value to embedders (e.g. InitOptions field or env var like FFF_LMDB_MAX_READERS) so hosts can tune without a recompile.
Workaround for affected users
Exit all processes holding slots (they are alive, so only process exit frees them), then optionally delete frecency.mdb/lock.mdb while nothing is running (the lock file is rebuilt on next open; frecency data in data.mdb is untouched). Starting the newest fff first also helps: it reclaims dead-PID slots on open, but it can not reclaim slots of the still-running old processes.
Summary
FFF's LMDB environments are opened without
max_readersand withoutMDB_NOTLS, so LMDB runs in default TLS mode: a reader slot is pinned per thread and only released when the process exits. In long-lived embedding processes (Neovim, Node-based agents viafff-node, …) that accumulate more than 126 reader threads over days, the reader table fills up and every subsequent env open fails:This is the complementary case to #460:
clear_stale_readers()(added in #468) reclaims slots of dead PIDs, but in our incident all slot holders were alive — 7 long-lived processes holding 15–19 slots each, 126/126 total:So the fix from #468 cannot help here — nothing is stale.
Environment
@ff-labs/fff-node0.10.3 (libfff_c.sofrom@ff-labs/fff-bin-linux-x64-gnu0.10.3)picoding agent + pi-pretty 0.6.21 (frecency/history at~/.pi/agent/pi-pretty/fff/)Evidence
Reader-table forensics on
frecency.mdb/lock.mdb(8 KiB page, 64-byte slots → 126 slots + header, matching the LMDB default):Controlled probes (throwaway DBs, small tree):
FileFinder→ exactly 3 slots (main + 2 native threads), stable across cycles — slots are per-thread, not per-finder or per-open. Env pooling from fix(pi-fff): stop reopening main LMDB envs in aux finders (#700) #701 works.Current code
On
main,EnvOpenOptionsonly setsmap_size(+max_dbs):crates/fff-core/src/dbs/env_pool.rs(~L95–99)crates/fff-core/src/dbs/lmdb.rs(open_env, ~L141–147)No
max_readers(...), no NOTLS flag.clear_stale_readers()on open (env_pool.rs) only helps dead PIDs.Suggestions
max_readersexplicitly (e.g. 256–512). Reader slots are tiny; the cost is negligible, and it buys embedding hosts a lot of headroom.MDB_NOTLSso read transactions borrow a slot from the pool only for the txn lifetime instead of pinning one per thread. This is the standard recommendation for hosts with transient/many threads (libuv threadpool, LSP-style worker churn). Caveat: in NOTLS mode a read txn must begin and end on the same thread — since fff owns its txn lifecycles insidefff-core, this should be enforceable there.InitOptionsfield or env var likeFFF_LMDB_MAX_READERS) so hosts can tune without a recompile.Workaround for affected users
Exit all processes holding slots (they are alive, so only process exit frees them), then optionally delete
frecency.mdb/lock.mdbwhile nothing is running (the lock file is rebuilt on next open; frecency data indata.mdbis untouched). Starting the newest fff first also helps: it reclaims dead-PID slots on open, but it can not reclaim slots of the still-running old processes.Related
clear_stale_readers+ automatic compactionsscripts/diagnose_lockmdb.py— diagnoses mutex holders/waiters; the slot-count snippet above complements it for reader-slot accounting