"""Live-sync supervisor (refactor §4, transitional). Runs inside the sync worker: on startup it launches one :class:`AccountRunner` per live account. This step (v2 §9.2) only extracts the per-account loops into a runner; account discovery is still startup-only and is replaced by the lease claimer in a later step. """ from __future__ import annotations import asyncio import logging import uuid from typing import Any from sqlalchemy import select from app.db import session_scope from app.models import Account from app.sync.runner import AccountRunner logger = logging.getLogger(__name__) async def _live_account_ids() -> list[uuid.UUID]: async with session_scope() as session: return list(await session.scalars(select(Account.id).where(Account.state == "live"))) async def start_supervisor(ctx: dict[str, Any]) -> None: """Launch an AccountRunner per live account (called from the worker on_startup).""" runners: dict[uuid.UUID, AccountRunner] = {} for account_id in await _live_account_ids(): runner = AccountRunner(account_id) runner.start() runners[account_id] = runner ctx["runners"] = runners logger.info("live supervisor started for %d account(s)", len(runners)) async def stop_supervisor(ctx: dict[str, Any]) -> None: """Stop and tear down all account runners (called from the worker on_shutdown).""" runners: dict[uuid.UUID, AccountRunner] = ctx.get("runners", {}) if runners: await asyncio.gather(*(r.stop() for r in runners.values()), return_exceptions=True) ctx["runners"] = {}