fix(gateway): /loop paths warm the SessionDB cache off-loop
The loops delegation (previous commit) moved /loop onto the shared bootstrap windows, but the loop-class gateway callers still constructed LoopManager on the loop thread with no warm-up — the same false-ack class as /goal, one sibling over: - The /loop command handler: a cold init past the window made save_loop discard the write while the reply claimed the loop was set. Reproduced with a 2s init: "↻ Loop set" with nothing persisted; with the warm-up the loop persists. - _post_turn_loop_completion: a cold cache at the turn boundary stalled the loop for the init duration and could drop the tick-completion write. - _loop_wakeup_watcher: the scan reads every persisted loop, so a cold cache ran the state.db init on the loop thread before the first read. All three now warm the cache off-loop first (same helper, same reason as the goal paths). save_loop also logs at WARNING when it drops a write, matching save_goal: the reply has already told the user the loop was set. Review findings (PR #88965): the goal and heartbeat command paths warmed off-loop, the loop-class paths did not.
This commit is contained in:
@@ -21397,6 +21397,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
||||
if not sid:
|
||||
return
|
||||
|
||||
# Warm the SessionDB cache off-loop. A cold cache at the turn
|
||||
# boundary stalls the loop for the init duration and can drop
|
||||
# the tick-completion write (the /goal continuation seam, one
|
||||
# sibling over).
|
||||
await self._warm_goals_session_db("loop completion")
|
||||
|
||||
mgr = LoopManager(session_id=sid)
|
||||
state = mgr.state
|
||||
if state is None or not state.awaiting_response:
|
||||
@@ -21435,6 +21441,11 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
||||
list_active_loops,
|
||||
)
|
||||
|
||||
# Warm the cache off-loop once per scan. The scan reads
|
||||
# every persisted loop, so a cold cache runs the state.db
|
||||
# init on the loop thread before the first read.
|
||||
await self._warm_goals_session_db("loop wakeup")
|
||||
|
||||
now = time.time()
|
||||
for sid, state in list_active_loops():
|
||||
if state.awaiting_response or now < state.next_due_at:
|
||||
|
||||
@@ -3038,6 +3038,10 @@ class GatewaySlashCommandsMixin:
|
||||
except Exception as exc:
|
||||
logger.debug("loop manager unavailable: %s", exc)
|
||||
return None, None
|
||||
# Warm the SessionDB cache off-loop. A cold cache drops the first
|
||||
# /loop write while the reply claims the loop was set (same class
|
||||
# as the /goal false-ack fix).
|
||||
await self._warm_goals_session_db("loop manager")
|
||||
try:
|
||||
session_entry = await self.async_session_store.get_or_create_session(event.source)
|
||||
except Exception:
|
||||
|
||||
@@ -415,6 +415,12 @@ def save_loop(session_id: str, state: LoopState) -> None:
|
||||
return
|
||||
db = _get_session_db()
|
||||
if db is None:
|
||||
logger.warning(
|
||||
"LoopManager: loop for %s not persisted — session DB "
|
||||
"unavailable (bootstrap window exceeded, in-memory state "
|
||||
"still active)",
|
||||
session_id,
|
||||
)
|
||||
return
|
||||
try:
|
||||
db.set_meta(_meta_key(session_id), state.to_json())
|
||||
|
||||
Reference in New Issue
Block a user