fix(state): WAL probe fallback, bare-SELECT locking, writable_schema finally, read-pool self-heal - #87612
fix(state): WAL probe fallback, bare-SELECT locking, writable_schema finally, read-pool self-heal#87612wanliqin wants to merge 4 commits into
Conversation
…e probe When _on_disk_journal_mode probing fails (most likely a concurrent opener holding a lock), _apply_delete_for_wal_reset_bug correctly leaves the journal mode alone but returned "wal". The caller sets _wal_active = apply_wal_with_fallback(...) == "wal", so _read_ctx enabled the lock-free mode=ro read pool. If the DB is actually in DELETE rollback-journal mode, readers skip the self._lock serialized path and hit raw SQLITE_BUSY during writes — random read failures for the instance's lifetime. Return "delete" for the indeterminate case instead. Claiming "delete" costs only queuing on the write lock (slow but correct); claiming "wal" costs unserialized read errors. Closes #86515
get_compression_lock_holder, the clear_session_activity_labels fast path, get_handoff_state, and list_pending_handoffs executed bare self._conn.execute(SELECT ...) on the instance-wide shared write connection. When another thread is inside _execute_write's BEGIN IMMEDIATE transaction, these SELECTs join that uncommitted transaction and can read rows that later roll back; in non-WAL (DELETE) mode they also collide with writers and surface false "no handoff" negatives. Route all four through _read_ctx, which degrades to the self._lock serialized path automatically when WAL is inactive — matching every other read path in the module. Closes #86516
…ote leaves no residue _demote_legacy_fts_to_trash._stage ran PRAGMA writable_schema=ON, the sqlite_master DELETE, then =RESET as a bare sequence inside _execute_write. writable_schema is a connection-level switch that does not roll back with the transaction, so if the DELETE raised (the demote path exists precisely for pathological DBs where sqlite_master may be inconsistent) the rollback left writable_schema=ON on the long-lived self._conn until process exit: subsequent schema parsing skips integrity checks, masking corruption is_malformed_db_error would otherwise catch. Wrap the DELETE in try/finally so RESET always runs. Closes #86517
… them _read_ctx's finally unconditionally returned the pooled mode=ro connection to the LifoPool, whether the query succeeded or raised. After the backing file is replaced or truncated (the scenario _reconnect_after_notadb's docstring lists: forked curator inheriting and closing the write fd, external repair pass), pooled read connections fail persistently — a truncated file raises 'file is not a database' on every query, and a POSIX rename-replace keeps the connection reading the old inode, silently returning stale data. LIFO order guarantees the next checkout gets the same broken connection, and _read_open_failed_at backoff only covers open failures, not query failures. The write connection has a one-shot reconnect self-heal; the read pool had none, so the fault persisted until process restart. On sqlite3.DatabaseError, destroy the connection via _close_read_conn (which also releases its descriptor permit) instead of returning it to the pool, so the next checkout reopens — the read-side counterpart of the write-side self-heal. Closes #86518
PR: fix(state): WAL probe fallback, bare-SELECT locking, writable_schema finally, read-pool self-heal
|
Summary
Four independent SQLite/state-layer fixes, one commit each:
"wal"wrongly enables the lock-free read pool; the conservative"delete"keeps reads serialized through_lock. Closes state: WAL-reset fallback returns 'wal' when journal-mode probe is indeterminate, wrongly enabling lock-free read pool #86515_read_ctx—get_compression_lock_holder, theclear_session_activity_labelsfast path,get_handoff_state, andlist_pending_handoffsread on the shared write connection and can observe uncommitted data. Closes state: four read paths bypass self._lock / _read_ctx and read on the shared write connection (dirty reads of uncommitted transactions) #86516PRAGMA writable_schema=ONlatched on the long-lived write connection when the DELETE raised. Closes state_search: writable_schema=ON window without try/finally leaves the long-lived write connection degraded on error #86517_read_ctxnow mirrors the write side's_reconnect_after_notadbself-heal: aDatabaseErrorcheckout is closed (permit released) instead of being returned to the pool. Closes state: read connection pool never invalidates broken connections (asymmetric with write-side _reconnect_after_notadb self-heal) #86518Tests
359 passed / 19 skipped across 15 related test files (hermes_state, session_db read pool/path split, state_db malformed/notadb self-heal, FTS search), plus 150 passed in
tests/hermes_state/. No failures.