fix(gateway): skip the executor hop for single-profile watcher scope resolves
The off-loop scope resolve hopped to the executor on every handoff (2s) and loop-wakeup (15s) tick, even in the default single-profile mode where _handoff_watch_scopes does no I/O and returns [(None, None)]. The executor is unbounded (one thread per work item), so that spawned ~34 OS threads a minute for no work. One helper next to _handoff_watch_scopes now returns the root poll directly when multiplex is off and only hops for the multiplex filesystem walk; both watchers use it, replacing the local _resolve_scopes closure. Config-less test stand-ins still resolve via the patched resolver. Also give the run_goals half teeth: the loop watcher's profile-gate test patched the resolver with a lambda that recorded nothing, so reverting run_goals stayed green. It now runs with multiplex on (required by the short-circuit), records the calling thread and asserts off-loop; red on the pre-fix run_goals.py. Co-authored-by: Emir Saffar <emir.saffar@uropenn.se>
This commit is contained in:
@@ -1729,6 +1729,20 @@ def _handoff_watch_scopes(runner: object) -> list:
|
||||
return scopes
|
||||
|
||||
|
||||
async def _resolve_handoff_watch_scopes(runner: object) -> list:
|
||||
"""``_handoff_watch_scopes`` for an on-loop watcher tick. Multiplex resolution walks the filesystem
|
||||
(``profiles_to_serve``), so it hops to the executor; single-profile mode does no I/O and returns the
|
||||
root poll directly — no per-tick thread spawn on the unbounded executor. A config-less stand-in
|
||||
(tests) and a runner without the executor hop fall through to the plain resolver."""
|
||||
config = getattr(runner, "config", None)
|
||||
if config is not None and not getattr(config, "multiplex_profiles", False):
|
||||
return [(None, None)]
|
||||
offload = getattr(runner, "_run_in_executor_with_context", None)
|
||||
if callable(offload):
|
||||
return await offload(_handoff_watch_scopes, runner)
|
||||
return _handoff_watch_scopes(runner)
|
||||
|
||||
|
||||
async def _reclaim_stale(runner: object) -> None:
|
||||
"""Fail handoffs left in ``running`` by a gateway that died mid-dispatch (once per store at startup).
|
||||
``running`` is only set for one in-process dispatch, so a leftover row belongs to a dead process and
|
||||
|
||||
@@ -473,7 +473,7 @@ class GatewayAdapterLifecycleMixin:
|
||||
"""Process pending CLI→gateway session handoffs from ``state.db``: claim atomically (pending
|
||||
→ running), re-bind the home channel to the CLI session_id, dispatch a synthetic event, mark
|
||||
``completed``/``failed``."""
|
||||
from gateway.run import _async_profile_runtime_scope, _handoff_watch_scopes, _reclaim_stale
|
||||
from gateway.run import _async_profile_runtime_scope, _reclaim_stale, _resolve_handoff_watch_scopes
|
||||
from gateway.run_idle_gates import off_loop_gate, profile_has_pending_handoff
|
||||
await asyncio.sleep(5) # let platforms connect before dispatching through them
|
||||
# Does _process_handoff accept the profile argument? Test stand-ins bind a one-arg callable.
|
||||
@@ -534,22 +534,16 @@ class GatewayAdapterLifecycleMixin:
|
||||
def _scope(profile_home): # local: tests bind this watcher onto bare SimpleNamespace runners
|
||||
return GatewayAdapterLifecycleMixin._async_scope_or_null(_async_profile_runtime_scope, profile_home)
|
||||
|
||||
# Resolve watch scopes off the loop: the profiles_to_serve() filesystem walk can stall the
|
||||
# loop past the liveness probe. Bare stand-ins without the executor hop resolve inline.
|
||||
offload = getattr(self, "_run_in_executor_with_context", None)
|
||||
|
||||
async def _resolve_scopes():
|
||||
return (await offload(_handoff_watch_scopes, self) if callable(offload)
|
||||
else _handoff_watch_scopes(self))
|
||||
|
||||
for _pname, _phome in await _resolve_scopes():
|
||||
# Multiplex scope resolution walks the filesystem (profiles_to_serve) off the loop, so a
|
||||
# stalled walk cannot trip the liveness probe — startup reclaim and every tick alike.
|
||||
for _pname, _phome in await _resolve_handoff_watch_scopes(self):
|
||||
with _log_suppressed(logging.DEBUG, "Stale-handoff reclaim failed", exc_info=True):
|
||||
async with _scope(_phome):
|
||||
await _reclaim_stale(self)
|
||||
try:
|
||||
while self._running:
|
||||
try:
|
||||
for profile_name, profile_home in await _resolve_scopes():
|
||||
for profile_name, profile_home in await _resolve_handoff_watch_scopes(self):
|
||||
# Idle gate (run_idle_gates): skip the scope entry when the profile's store
|
||||
# holds no pending handoff. The root poll (None) is unscoped and stays cheap.
|
||||
if profile_home is not None and not await off_loop_gate(
|
||||
|
||||
@@ -460,7 +460,7 @@ class GatewayGoalsMixin:
|
||||
store — a ``/loop`` set from a secondary profile's chat would never fire. Every served
|
||||
profile's store is scanned under its own runtime scope (same shape as ``_handoff_watcher``),
|
||||
and each hit is fired against that profile's adapters."""
|
||||
from gateway.run import _async_profile_runtime_scope, _handoff_watch_scopes
|
||||
from gateway.run import _async_profile_runtime_scope, _resolve_handoff_watch_scopes
|
||||
from gateway.run_idle_gates import profile_has_active_loop
|
||||
await asyncio.sleep(5) # let platforms finish connecting
|
||||
warned_no_route: set = set()
|
||||
@@ -487,10 +487,9 @@ class GatewayGoalsMixin:
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
# Off-loop: profiles_to_serve() walks the filesystem (realpath chains + profile-dir
|
||||
# scans) every pass; a stalled walk trips the loop-liveness watchdog (exit 75).
|
||||
scopes = await self._run_in_executor_with_context(_handoff_watch_scopes, self)
|
||||
for profile_name, profile_home in scopes:
|
||||
# Multiplex resolution walks the filesystem off-loop; a stalled walk on the loop
|
||||
# trips the loop-liveness watchdog (exit 75).
|
||||
for profile_name, profile_home in await _resolve_handoff_watch_scopes(self):
|
||||
# Idle gate (run_idle_gates): skip the scope entry when the profile's store holds
|
||||
# no active loop. The root scan (None) is unscoped and stays cheap.
|
||||
if profile_home is not None and not await self._run_in_executor_with_context(
|
||||
|
||||
Reference in New Issue
Block a user