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).
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user