fix(state): close threaded SessionDB read conns without leaking FDs - #74304
fix(state): close threaded SessionDB read conns without leaking FDs#74304jmeadlock wants to merge 3 commits into
Conversation
Opus review (Nous Portal
|
|
Reconsidered and tightened this PR before requesting CI/review:
Local verification: targeted repository runner 10/10, Ruff clean, and |
|
Thanks for the focused lifecycle fix. Current The regression test exercises the relevant completed-worker shutdown contract rather than relying on platform-specific FD counting. Current tests already establish that reader connections are created per worker thread ( Automated hermes-sweeper review. |
|
Confirmed the premise here independently on macOS — the cross-thread drain does raise, universally: And the descriptor consequence, measured with One thing worth pairing with this PR: #75629. The swallowed This PR removes the dominant cause of that failed close, which is genuinely most of the exposure. But the ordering bug survives it: any other close failure (I/O error, an already-closed handle, a caller-supplied connection factory) still under-counts the registry, and that registry is a corruption-safety guard rather than bookkeeping. Suggest landing #75629 alongside so the guard is correct regardless of why a close failed — otherwise this PR has the side effect of making the remaining ordering bug much harder to ever observe. Also relevant to the "not claimed fixed by this PR" section: #75269 covers the pre-shutdown retention (readers from finished threads never reaped while the shared |
|
Thank you for isolating the cross-thread reader-connection leak. I independently confirmed that The independent review was bound to baseline A reconciled solution still needs to ensure the full lifecycle matrix: route the compression-holder read through A reviewed two-file candidate covering these lifecycle changes was tested against the byte-identical source blobs. The unchanged source produced 14 passes and 8 intended focused failures. The candidate passed 22 focused tests and 200 broader related tests; a repeat stress probe passed 20/20. Ruff, static, and security checks passed, and an independent exact-diff review reported no blocking findings. Two newer normal-lifetime retention proposals also overlap this area. #75546's current patch adds bounded generation eviction, per-connection RLock serialization, Rather than publish a competing replacement, I would prefer to preserve this PR's authorship and coordinate it with #73803, #75546, and #76424. #73803 contributes the handoff-read race reproduction, routing, diagnostics, and WAL-path coverage; #75546 and #76424 contribute overlapping normal-lifetime retention approaches. Would the authors and maintainers prefer these overlapping changes to be reconciled first, with explicit credit preserved for each contributor's original work, followed by one narrow contribution for only the remaining lifecycle matrix? I can provide the deterministic regressions and reviewed reference patch. Its SHA-256 is |
You are asking how this repository should reconcile several overlapping lifecycle changes while preserving each contributor's credit and avoiding a competing replacement. Case context, measured live from our triage graph (2026-08-02T01:57:00+00:00):
If you want to move this one along: keep the diff scoped and rebase onto current |
Per-thread WAL read connections in SessionDB._get_read_conn omitted check_same_thread=False. SessionDB.close() runs on the owner thread, so every worker-created read conn raised sqlite3.ProgrammingError, was swallowed, and leaked .db/.wal/.shm descriptors. Gateway and dashboard share one SessionDB across thread pools; this compounded into EMFILE / "too many open files" on macOS soft limits. Match the writer and read_only open paths: open read conns with check_same_thread=False. Add regression tests for cross-thread close and thread-pool read cycles reclaiming FDs.
Address Opus review on NousResearch#74304: - Tests now fail on cross-thread ProgrammingError (the real pre-fix defect) and assert process num_fds drops on close. Path-based open_files() counting under-reports SQLite fds on macOS and would green-light the broken branch. - SessionDB.close() logs ProgrammingError at warning when a per-thread read conn cannot be drained, so this leak class cannot go silent again.
Replace process-wide FD assertions and multi-thread choreography with one direct cross-platform contract: a reader created by a finished worker must be closed by SessionDB.close().\n\nNarrow the comments and warning text to the confirmed reader lifecycle bug class without attributing the entire dashboard incident to this patch.
00eccf0 to
2dcb187
Compare
|
Rebased onto current Scope is unchanged:
Local verification on the rebased head:
GitHub currently reports no CI checks for this branch, so those results are local-only. I’m keeping this PR limited to the completed-worker shutdown contract: cross-thread-closeable WAL readers, a visible warning if the shutdown drain still hits thread affinity, and the focused regression. The normal-lifetime cache/eviction, dead-thread pruning, quiesce, and handoff-read work should stay with #75546, #76424, and #73803 rather than growing this diff. I have no preference about authorship or which PR lands first—whatever gets the bug fixed cleanly. Could maintainers make a merge-order call so these branches stop overlapping? If a verified superset lands first, I’m happy to close this one; whichever path wins should retain the completed-worker shutdown regression. @kwlfmarketing: could you publish the reference patch as a branch or attach the diff? A SHA-256 and test counts aren’t enough to compare or reproduce it. If the actual patch supersedes this cleanly, that makes the decision easy. |
|
Resolved on main by PR #83406 (rebase-merged), which landed a bounded read-connection pool for SessionDB: reads borrow from a LIFO pool (max 8) with a lifetime permit per open connection, bounding PEAK descriptors, and close() drains from any thread. Your diagnosis of the (SessionDB x thread) accumulation was correct — thank you for working on this. The pool approach from #76700 was chosen among the four competing fixes because it bounds peak descriptors rather than reaping after the fact; @Yishova's commits carry the class fix, with credit to all four submitters for converging on the root cause. |
Summary
Fixes a specific file-descriptor leak in
SessionDB's per-thread WAL read path.SessionDB._get_read_conn()creates read connections on worker threads, whileSessionDB.close()drains the tracked connections from its caller after those workers finish. The read connections used SQLite's defaultcheck_same_thread=True, so owner-thread shutdown raisedsqlite3.ProgrammingError. Because shutdown swallowed that exception, the SQLite.db/.wal/.shmdescriptors remained open.The observed behavior is consistent with the repeated SQLite handles seen in long-lived gateway/dashboard processes. This PR fixes that confirmed
SessionDBlifecycle bug; it does not claim to address every possible source of process FD growth.Fix
check_same_thread=False, matching the existing writer and cross-profile read-only connection policy.SessionDB.close()still encounters a thread-affinityProgrammingErrorwhile draining a reader.The lifecycle contract covered here is shutdown after the worker using the connection has finished. This patch does not introduce a new concurrent-close protocol for an in-flight query.
The read connections remain thread-local during normal use; the cross-thread reference exists only in the shutdown drain set. The runtime SQLite build is serialized (
THREADSAFE=1), ordinary overlapping SQL close waits for the query to finish, and these connections register no Python SQLite callbacks (create_function,create_aggregate,create_collation,set_progress_handler,set_trace_callback, orset_authorizer).sqlite3.Rowis the only read-path row factory. Existing_read_conns_closedhandling immediately closes and rejects a reader opened after shutdown begins.Regression coverage
The test exercises the behavior directly and without platform-specific FD APIs:
SessionDB.close().closed database.The same invariant fails on upstream
mainwith the original same-threadProgrammingErrorand passes on this branch.Test plan
scripts/run_tests.sh tests/test_session_db_read_path_split.py— 10 passeduv run ruff check hermes_state.py tests/test_session_db_read_path_split.pygit diff --checkRelated reports — not claimed fixed by this PR