sync-v2 step 2: extract AccountRunner
Move the per-account INBOX-IDLE + secondary-poll loops out of the supervisor into a self-contained AccountRunner (start/stop/teardown). The startup supervisor now launches one runner per live account. Pure refactor, no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""Per-account sync runner (refactor §4).
|
||||
|
||||
An ``AccountRunner`` owns the long-lived sync loops for a single account and
|
||||
exposes a small ``start`` / ``stop`` lifecycle. This is a pure extraction of the
|
||||
loops that previously lived inline in the supervisor — behavior is unchanged
|
||||
(INBOX IDLE + secondary CONDSTORE poll, each self-healing with backoff).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from app.sync.engine import live_loop, secondary_poll_loop
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_RECONNECT_BACKOFF = 30.0
|
||||
|
||||
|
||||
class AccountRunner:
|
||||
"""Owns the sync loops for one account until stopped."""
|
||||
|
||||
def __init__(self, account_id: uuid.UUID) -> None:
|
||||
self.account_id = account_id
|
||||
self._stop = asyncio.Event()
|
||||
self._tasks: list[asyncio.Task[None]] = []
|
||||
|
||||
def start(self) -> None:
|
||||
"""Launch the account's sync loops (idempotent-ish: call once)."""
|
||||
self._stop.clear()
|
||||
self._tasks = [
|
||||
asyncio.create_task(self._supervise(self._run_live, "live")),
|
||||
asyncio.create_task(self._supervise(self._run_secondary, "secondary")),
|
||||
]
|
||||
|
||||
def running(self) -> bool:
|
||||
return any(not t.done() for t in self._tasks)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Signal and cancel the loops, waiting for teardown."""
|
||||
self._stop.set()
|
||||
for task in self._tasks:
|
||||
task.cancel()
|
||||
if self._tasks:
|
||||
await asyncio.gather(*self._tasks, return_exceptions=True)
|
||||
self._tasks = []
|
||||
|
||||
async def _run_live(self) -> None:
|
||||
await live_loop(self.account_id, "INBOX", self._stop)
|
||||
|
||||
async def _run_secondary(self) -> None:
|
||||
await secondary_poll_loop(self.account_id, self._stop)
|
||||
|
||||
async def _supervise(self, run: Callable[[], Awaitable[None]], label: str) -> None:
|
||||
"""Run a loop, restarting it (with backoff) until stopped."""
|
||||
while not self._stop.is_set():
|
||||
try:
|
||||
await run()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("%s loop for account %s crashed; retrying", label, self.account_id)
|
||||
with contextlib.suppress(TimeoutError):
|
||||
await asyncio.wait_for(self._stop.wait(), timeout=_RECONNECT_BACKOFF)
|
||||
Reference in New Issue
Block a user