fix(sessions): retire lost-generation writers unclosed where the close-time checkpoint cannot be switched off

With the retired generation captured durably, closing a lost-generation
handle is safe wherever SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE took effect: the
frames are preserved and sqlite3_close no longer checkpoints them into the
newer generation. On Python 3.11, where sqlite3 has no setconfig, closing
still runs SQLite's internal checkpoint over the newer main file, so only
there the exact quarantined connection is retained instead of closed: one
public Py_IncRef reference via ctypes.pythonapi (no struct-layout access),
bound before the writer opens, taken in close() after the capture. Runtimes
with setconfig never touch ctypes; a writable SessionDB requires CPython
with ctypes only where retention is the sole guard.

Read-only handles and every other quarantine reason close as before.
Regressions cover both branches: where retention applies, the retired inode
stays readable after close() and GC and an independent deleted WAL under
the same pathname is untouched; elsewhere the capture is the surviving copy.
A separate process's newer generation survives close, GC and normal exit
in both cases. The mock-based close-time regression originally written for

Refs #105670

Co-authored-by: fangliquanflq <fangliquan@qq.com>
This commit is contained in:
Totoro-qaq
2026-09-09 15:37:44 +08:00
committed by kshitij
parent d616424571
commit d93f72c460
4 changed files with 480 additions and 20 deletions
+28
View File
@@ -29,6 +29,34 @@ from hermes_state_common import (
# Log-record parity with the origin module (caplog tests pin "hermes_state").
logger = logging.getLogger("hermes_state")
def _prepare_connection_retirement():
"""Bind a non-finalizing reference before opening a writable SQLite handle.
A Python container is cleared during interpreter shutdown. An unmatched
CPython C reference keeps the exact connection alive through that cleanup,
so SQLite cannot checkpoint its lost WAL generation from a finalizer. This
deliberately retains the connection and its descriptors until process exit;
it does not make already-unlinked WAL data durable after the last fd closes.
"""
message = (
"Writable SessionDB on a Python without sqlite3 setconfig (< 3.12) requires CPython with "
"ctypes support to retain a quarantined SQLite connection through interpreter shutdown."
)
if sys.implementation.name != "cpython":
raise RuntimeError(message)
try:
import ctypes
# A private function object avoids changing another caller's signature.
retain = ctypes.pythonapi["Py_IncRef"]
retain.argtypes = (ctypes.py_object,)
retain.restype = None
except (ImportError, AttributeError, OSError) as exc:
raise RuntimeError(message) from exc
return retain
# _read_sqlite_application_id runs on EVERY write (_raise_if_db_replaced) against the LIVE
# state.db. A bare open()/read()/close() there is the howtocorrupt §2.2 bug: close() cancels
# every POSIX advisory lock this process holds on the file, dropping the writer's WAL-mode DMS