Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contributors/emails/luc@provencher.family
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Rroven
# PR #107701 test co-author
17 changes: 8 additions & 9 deletions hermes_cli/web_server_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,17 @@ def _resolve_restart_drain_timeout() -> float:


def _eager_reconcile_own_session_db() -> None:
"""One writable open of this process's own state.db at startup.

``SessionDB.__init__`` runs ``_init_schema`` → ``_reconcile_columns`` with
open-time lock patience. Never raises: an unfixable store still gets the
per-poll read-probe heal in :func:`_open_session_db_at_path`.
"""Heal a stale schema in this process's own state.db at startup — read-only, so the dashboard
never becomes a second writable owner beside the gateway (a writable open ran full schema init
plus a close-time checkpoint against its writer). Access-mode semantics: see
:func:`hermes_cli.web_server_sessions._open_session_db_at_path`. Never raises: an unfixable
store still gets the per-poll read-probe heal.
"""
try:
from hermes_state import _default_db_path
from hermes_state_registry import acquire, release_or_close
from hermes_cli.web_server_sessions import _open_session_db_for_profile
from hermes_state_registry import release_or_close

db = acquire(Path(_default_db_path()))
release_or_close(db)
release_or_close(_open_session_db_for_profile(None, read_only=True))
except Exception as exc:
_log.warning(
"startup schema reconcile of state.db failed (%s); session "
Expand Down
27 changes: 27 additions & 0 deletions tests/hermes_cli/test_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,33 @@ def boom(*args, **kwargs):
# Must swallow — reads fall back to the per-poll probe heal.
_web_server_lifecycle._eager_reconcile_own_session_db()

def test_startup_eager_reconcile_opens_read_only(self, monkeypatch):
"""A healthy store gets a read-only open (no second writable owner) and the handle is
released. The stale-schema heal is covered by test_startup_eager_reconcile_heals_stale_store."""
from pathlib import Path

import hermes_state
import hermes_cli.web_server_sessions as _web_server_sessions

calls = []
closed = []

def fake_open(db_path: Path, *, read_only: bool):
calls.append((str(db_path), read_only))
return SimpleNamespace(close=lambda: closed.append(True))

monkeypatch.setattr(hermes_state, "_default_db_path", lambda: "/tmp/fake-state.db")
monkeypatch.setattr(
_web_server_sessions, "_open_session_db_at_path", fake_open,
)

_web_server_lifecycle._eager_reconcile_own_session_db()

assert calls == [("/tmp/fake-state.db", True)], (
"eager reconcile must open state.db read-only on a healthy store"
)
assert closed == [True], "the startup handle must be released, not leaked"

def test_heal_gives_up_when_reconcile_cannot_fix_the_store(self, monkeypatch):
"""A probe failure reconciliation can't cure must not retry forever.

Expand Down
Loading