Files
CIAgent/apps/api/app/tasks/base.py
T
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
2026-08-05 10:48:20 -04:00

34 lines
1.4 KiB
Python

"""Bridges Celery's sync task functions into the app's async service layer.
In production, a Celery worker process has no asyncio event loop running
when a task executes, so a plain `asyncio.run(...)` is enough. But with
`CELERY_TASK_ALWAYS_EAGER=true` (tests, and `.delay()` called from inside an
async FastAPI route handler), the task body runs synchronously *inside* the
caller's already-running event loop, and `asyncio.run()` refuses to nest.
`run_async_task` handles both: it uses `asyncio.run()` directly when no loop
is running, and falls back to a dedicated thread with its own loop when one is.
"""
from __future__ import annotations
import asyncio
import contextvars
from collections.abc import Coroutine
from concurrent.futures import ThreadPoolExecutor
def run_async_task[T](coro: Coroutine[object, object, T]) -> T:
try:
asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(coro)
# ThreadPoolExecutor does not copy contextvars into the worker thread by
# default, which would silently drop the structlog correlation id
# (request_id/run_id/task_id - see core/logging.py) bound by the caller.
# Capturing the current context explicitly and running the executor call
# through it keeps those log fields intact even on this fallback path.
ctx = contextvars.copy_context()
with ThreadPoolExecutor(max_workers=1) as executor:
return executor.submit(ctx.run, asyncio.run, coro).result()