fix(state): pooled read connections survive transient EIO (WSL2/vhdx, ZFS) - #100883
Sahilvishnaliya wants to merge 1 commit into
Conversation
andrexibiza
left a comment
There was a problem hiding this comment.
Blocking issues on exact head 6b453e018ff8fe84c1f9b60f648be741dd87ae29:
-
P1 —
read_execute()returns a cursor after releasing its pooled connection. Bothreturn conn.execute(...)paths exit_read_ctx()before the caller executescursor.fetchone(). That means the connection has already been returned to_read_pooland can be borrowed concurrently while the cursor is still being stepped. It also places the exactfetchone()/SQLite-step failure outside the EIO retry boundary, so an IOERR raised while stepping still escapes and the connection is not marked poisoned. The competing #100882 explicitly covers this boundary with execute+fetch under one checkout. This carrier needs the same ownership invariant: acquire → execute → consume the row/result → classify any EIO → only then recycle/close the connection. Do not return a live cursor across the pool lease. -
P1 — the regression fixture hard-codes a developer checkout into import resolution.
tests/test_read_pool_eio_retry.pydoessys.path.insert(0, r"C:\\Users\\salma\\dev\\hermes-agent"). On that machine it can import a different checkout than the PR under test; elsewhere it silently points at a nonexistent path. Tests must exercise the checked-out repository object, not an ambient personal path. -
2K / ownership gate — this adds another read-pool lifecycle mechanism directly to
hermes_state.py, already far beyond the repository's 2,000-line ceiling. The retry/poison lifecycle should have one bounded owner rather than further expanding the state godfile, especially with #100882 competing for the same semantic surface.
The fresh-connection direction may still be the right #100871 strategy, but these ownership/proof defects have to be resolved before this object can certify it.
… ZFS) The pooled read machinery had no EIO recovery: when a pooled connection threw 'disk I/O error' the exception propagated straight out of get_session, and the poisoned connection was returned to the pool unchanged. This replaces read_execute with read_fetch so statement execution and row fetching occur entirely inside the _read_ctx pool lease, avoiding returning a live cursor across the pool checkout boundary.
6b453e0 to
6d2d524
Compare
|
@andrexibiza I have updated the PR to address all feedback:
|
…fore surfacing it Since 0.21.0 reads go through mode=ro pooled connections. A read-only OPEN already rides out the millisecond WAL transition window (checkpoint / WAL reset / frame flush by a sibling process; the ro reader cannot rewrite the -shm index) with a bounded retry (#100436), but a WARM pooled reader hitting the same window while its SELECT executes propagated `disk I/O error` straight out of get_session(): 37 identical tracebacks on a multi-process WSL2 ext4-on-vhdx install, each followed by "compression session recovery failed", with quick_check=ok (#100871). The reporter's A/B shows the operator workaround (journal_mode=delete) collapses read throughput ~30000x, so the flake has to be absorbed on the read path. _read_one/_read_all now replay the idempotent statement within the existing read-only IOERR budget (3 x 50 ms) on the SAME connection -- close+reopen would cancel this process's POSIX locks for every sibling connection -- and a persistent IOERR still propagates. No quarantine: EIO on a read is busy, not broken. Every SELECT in the SessionDB siblings (63 call sites) reaches the pool through these two helpers, so the class is covered without a wrapper type. Same-connection retry per #100882's analysis (@fangliquanflq); #100883 (@Sahilvishnaliya) diagnosed the missing recovery in the 0.21.0 read pool. Fixes #100871. Co-authored-by: fangliquanflq <fangliquan@qq.com> Co-authored-by: Sahilvishnaliya <222165401+Sahilvishnaliya@users.noreply.github.com>
|
Landed on main in #108082 (d956e05). The transient-IOERR retry landed as a same-connection retry in |
Fixes #100871
Problem
The 0.21.0 pooled read machinery had no EIO recovery. When a pooled read connection threw
disk I/O error:get_session— the reporter's multi-process WSL2 install (ext4-on-vhdx,sparseVhd=true) produced 37 identical tracebacks, each followed bycompression session recovery failedField evidence: zero occurrences in the two weeks before 0.21.0, onset minutes after the upgrade (
git blameshows the read-pool machinery as what changed underneath unchanged SQL),PRAGMA quick_check= ok, not fd exhaustion (42/4096), not disk-full (900+ GB).Their controlled A/B also rules out the operator workaround:
journal_mode: deletecollapses read throughput ~30000x (5,139,477 → 174 reads) because DELETE serializes readers behind the writer. Absorbing the flake in the read pool is the only usable mitigation.Fix — two coordinated changes
1.
_read_ctxpoisons-are-closed, not recycled. A pooled connection that raised a transient EIO is closed and its fd permit released, instead of going back into the pool. On CoW/sparse backing stores one bad shared-memory interaction poisons that specific handle while the DB stays intact.2.
read_execute(sql, params)— once-only EIO retry on a fresh connection.get_session, the exact crash site of all 37 tracebacks, now goes through it. Deterministic errors (schema, corruption) propagate on the first attempt with no retry; EIO on both attempts propagates too. Same retry shape as the existing_on_disk_journal_modeloop and the write path.Observability: absorbed retries counted (
_read_ioerr_retries) and each logs a WARNING naming the db path — "flake absorbed" is distinguishable from "hard failure" in the logs.Verification
tests/test_read_pool_eio_retry.py— 5/5 pass, driving real pooled connections (poisoned via per-connection execute override so the fd-permit accounting stays honest):no such tablepropagates on attempt one, 0 retriesget_sessionsurvives transient EIO (the 37-traceback crash site)_close_read_conn)Sibling pool/read suites all green:
test_session_db_read_conn_pool.py+test_session_db_read_path_split.py+test_hermes_state_conn_lock_audit.py30/30,test_state_db_malformed_repair.py21 passed / 3 skipped.