from __future__ import annotations import asyncio import signal import socket import uuid from app.config import load_settings from app.crm import CrmClient from app.domain import full_jitter_delay from app.engine import RetryableWorkflow, WorkflowEngine from app.repository import Repository async def worker_main() -> None: settings = load_settings() if not settings.enabled: return assert settings.database_url and settings.crm_rest_webhook_url and settings.portal_host repository = Repository(settings.database_url.get_secret_value(), settings.db_pool_size) crm = CrmClient( settings.crm_rest_webhook_url.get_secret_value(), settings.portal_host, settings.http_timeout_sec, ) engine = WorkflowEngine(repository, crm, settings) stop = asyncio.Event() loop = asyncio.get_running_loop() for event in (signal.SIGINT, signal.SIGTERM): try: loop.add_signal_handler(event, stop.set) except NotImplementedError: pass worker_id = f"{socket.gethostname()}:{uuid.uuid4()}" try: while not stop.is_set(): tasks = await repository.claim_tasks( worker_id, settings.claim_size, settings.lease_seconds ) webhooks = await repository.claim_webhooks( worker_id, settings.claim_size, settings.lease_seconds ) rebind_ids = await repository.pending_rebind_ids(settings.claim_size) if not tasks and not webhooks and not rebind_ids: try: await asyncio.wait_for(stop.wait(), timeout=1) except TimeoutError: continue for task in tasks: if stop.is_set(): break await engine.process(task) for item in webhooks: if stop.is_set(): break try: await engine.process_webhook(item) except RetryableWorkflow as exc: delay = exc.retry_after or full_jitter_delay( item.attempt_count + 1, settings.retry_base_seconds, settings.retry_max_seconds, ) await repository.retry_webhook(item, exc.code, delay) for request_id in rebind_ids: if stop.is_set(): break try: await engine.process_rebind(request_id) except RetryableWorkflow: continue finally: await crm.close() await repository.close() def run() -> None: asyncio.run(worker_main())