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).
34 lines
1.4 KiB
Python
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()
|