fix(sessions): a failed retired-generation capture still pins the 3.11 handle and logs at ERROR
Every production close of a SessionDB goes through hermes_state_registry.release_or_close, whose teardown swallows any exception from close() at DEBUG. With the capture in place that meant a RetiredGenerationCaptureError (disk full, permissions) left the handle open silently and, on Python 3.11, skipped the retention pin: the raise happened before the pin, so the interpreter's exit still ran sqlite3_close's implicit checkpoint and wrote the stale frames over the newer generation (probe: 302 rows -> 4 after exit, worse than main). - close() now decides retention first and takes the pin BEFORE attempting the capture on runtimes without setconfig; the capture failure is logged at ERROR and re-raised, so a later close() retries it while the pin already protects the newer generation. - The registry logs RetiredGenerationCaptureError at ERROR instead of DEBUG (other teardown errors stay quiet). Same treatment on the release_or_close fallback path. - The lost-generation settlement moves out of close() into _settle_lost_generation_locked(); the sticky _close_checkpoint_disabled attribute and the dead "setconfig exists but failed" late-bind block are gone (_disable_close_time_checkpoint returns the call's outcome). Regression test drives release_or_close with a failing capture: no raise, error text in the log, pin taken exactly once (3.11), retry closes cleanly. Red on the salvaged head on both 3.11 and 3.12, green here.
This commit is contained in:
@@ -108,10 +108,20 @@ def _teardown(db: "SessionDB") -> None:
|
||||
"""Close a shared instance, clearing its registry-owned flag first."""
|
||||
with contextlib.suppress(Exception):
|
||||
db._shared_registry_owned = False
|
||||
_close_quietly(db, "Error closing shared SessionDB")
|
||||
|
||||
|
||||
def _close_quietly(db: "SessionDB", debug_message: str) -> None:
|
||||
"""close() that never propagates. A lost WAL generation whose capture failed is data at risk,
|
||||
not teardown noise: the handle stays open and the operator has to act, so that one surfaces."""
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
logger.debug("Error closing shared SessionDB", exc_info=True)
|
||||
except Exception as exc:
|
||||
from hermes_state_dbfile import RetiredGenerationCaptureError
|
||||
if isinstance(exc, RetiredGenerationCaptureError):
|
||||
logger.error("SessionDB for %s did not settle at close: %s", _db_path_of(db), exc)
|
||||
else:
|
||||
logger.debug(debug_message, exc_info=True)
|
||||
|
||||
|
||||
def _path_lifecycle_lock_locked(path: Path) -> threading.Lock:
|
||||
@@ -378,10 +388,7 @@ def release_or_close(db: "SessionDB") -> None:
|
||||
"""Release a shared instance, or close it when it is not registry-managed. Drop-in for a
|
||||
plain ``db.close()``: read-only opens, CLI one-shots and test fakes fall back."""
|
||||
if not release(db):
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
logger.debug("release_or_close fallback close failed", exc_info=True)
|
||||
_close_quietly(db, "release_or_close fallback close failed")
|
||||
|
||||
|
||||
# ---- BEGIN PLUGIN-COMPAT (revert-scheduled; see COMPAT_MANIFEST.md) ----
|
||||
|
||||
Reference in New Issue
Block a user