"""Durable worker loop. A standalone process that polls the durable ``jobs`` table, claims one job per iteration, dispatches to a registered handler, and records completion or failure. This is the portable core of the production worker topology and does not depend on a specific broker; a queue provider (Redis/RabbitMQ) can be added later by swapping the claim strategy without changing handlers. Run with, e.g.:: PYTHONPATH=backend uv run python -m worker --once PYTHONPATH=backend uv run python -m worker --poll-interval 1.0 """ import argparse import time from typing import Callable, Optional from app.db import create_database_engine, create_session_factory from app.services.job_queue import JobQueue def _make_queue(): engine = create_database_engine() session_factory = create_session_factory(engine) session = session_factory() return engine, session, session_factory def run_once(session, *, worker_id: str = "worker", organization_id: Optional[str] = None, **handlers) -> bool: """Claim and process one job; returns True when one was handled.""" queue = JobQueue(session) for operation, handler in handlers.items(): queue.register_handler(operation, handler) job = queue.claim_next_job(worker_id=worker_id, organization_id=organization_id) if job is None: session.rollback() return False try: result = queue.dispatch(job, payload=job.result) queue.complete_job(job.id, result=result) session.commit() except Exception as exc: # noqa: BLE001 - worker must not die on a bad job session.rollback() try: queue.fail_job(job.id, error_code=type(exc).__name__) session.commit() except Exception: # noqa: BLE001 session.rollback() return True def run_loop( *, poll_interval: float = 1.0, worker_id: str = "worker", organization_id: Optional[str] = None, handlers: Optional[dict[str, Callable]] = None, ) -> None: handlers = handlers or {} engine, session, _ = _make_queue() try: while True: handled = run_once( session, worker_id=worker_id, organization_id=organization_id, **handlers, ) if not handled: time.sleep(poll_interval) else: session = _fresh_session(engine, session) finally: session.close() engine.dispose() def _fresh_session(engine, session): try: session.close() except Exception: # noqa: BLE001 pass return create_session_factory(engine)() def main() -> None: parser = argparse.ArgumentParser(description="Durable job worker") parser.add_argument("--once", action="store_true", help="process a single job and exit") parser.add_argument("--poll-interval", type=float, default=1.0) parser.add_argument("--worker-id", default="worker") parser.add_argument("--organization-id", default=None) args = parser.parse_args() if args.once: engine, session, _ = _make_queue() try: run_once(session, worker_id=args.worker_id, organization_id=args.organization_id) finally: session.close() engine.dispose() else: run_loop( poll_interval=args.poll_interval, worker_id=args.worker_id, organization_id=args.organization_id, ) if __name__ == "__main__": main()