Ownership of an account now moves through a Redis lease so any number of sync
workers can run against the same account set with no double-IDLE.
- leases.py: sync:live registry + sync:lease:{id} (SET NX EX) with owner-checked
Lua renew/release; mark_live / mark_not_live helpers.
- coordinator.py: SyncCoordinator claims from sync:live up to MAX_ACCOUNTS_PER_
WORKER, renews every LEASE_RENEW (fences the runner on lease loss), reconciles
sync:live against Postgres, heartbeats a worker key, and rebalances when it
holds well above a fair share.
- supervisor start/stop now drive a coordinator; engine._set_state and
delete_account maintain sync:live membership (+ lease drop = fencing teardown).
- config: MAX_ACCOUNTS_PER_WORKER, LEASE_TTL, LEASE_RENEW.
- tests: acceptance 1-3 (single ownership, crash failover, create/delete
lifecycle) driving the real coordinator + Redis with faked IMAP loops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
30 lines
904 B
Python
30 lines
904 B
Python
"""Live-sync entrypoint (refactor §2, step 3).
|
|
|
|
The sync worker's ``on_startup`` starts a :class:`SyncCoordinator`, which claims
|
|
accounts from the ``sync:live`` registry via Redis leases and runs one
|
|
:class:`~app.sync.runner.AccountRunner` per owned account. ``on_shutdown`` stops
|
|
it, releasing every lease so peers can take the accounts over immediately.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from app.sync.coordinator import SyncCoordinator
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def start_supervisor(ctx: dict[str, Any]) -> None:
|
|
coordinator = SyncCoordinator()
|
|
await coordinator.start()
|
|
ctx["coordinator"] = coordinator
|
|
|
|
|
|
async def stop_supervisor(ctx: dict[str, Any]) -> None:
|
|
coordinator: SyncCoordinator | None = ctx.get("coordinator")
|
|
if coordinator is not None:
|
|
await coordinator.stop()
|
|
ctx["coordinator"] = None
|