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:
kshitijk4poor
2026-09-10 01:21:02 +05:30
committed by kshitij
parent d93f72c460
commit d87bf0d07e
3 changed files with 97 additions and 46 deletions
+13 -6
View File
@@ -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) ----