From 0477855efff9e80951245885a00e6c617f7742bc Mon Sep 17 00:00:00 2001 From: zealonexp Date: Sat, 2 May 2026 13:45:23 +0800 Subject: [PATCH] fix(cron): use real asyncio Task for live adapter delivery aiohttp ClientTimeout requires an asyncio Task context. The previous run_coroutine_threadsafe() wraps the callback in a Handle, not a Task, causing "Timeout context manager should be used inside a task" errors on Weixin delivery. Replace with call_soon_threadsafe + ensure_future to create a proper Task on the gateway event loop, preserving thread safety. Fixes: cron delivery to Weixin fails with timeout error Affected jobs: evolver-gene-extraction, evolver-gene-feedback --- cron/scheduler.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index 4672b24ba782b..00a5a2e688d71 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -436,10 +436,26 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> Option text_to_send = cleaned_delivery_content.strip() adapter_ok = True if text_to_send: - future = asyncio.run_coroutine_threadsafe( - runtime_adapter.send(chat_id, text_to_send, metadata=send_metadata), - loop, - ) + coro = runtime_adapter.send(chat_id, text_to_send, metadata=send_metadata) + # aiohttp's ClientTimeout requires an asyncio Task context. + # run_coroutine_threadsafe wraps the callback in a handle, + # not a Task — which breaks aiohttp's timeout on some platforms + # (e.g. Weixin). Wrap in a real Task via loop.call_soon_threadsafe + # + ensure_future for proper Task semantics. + import concurrent.futures as _cf # noqa: already imported at module level; local alias for clarity + _task_ready = _cf.Event() + _task_ref = [None] + + def _schedule(): + _task_ref[0] = asyncio.ensure_future(coro, loop=loop) + _task_ready.set() + + loop.call_soon_threadsafe(_schedule) + _task_ready.wait(timeout=5) + task = _task_ref[0] + if task is None: + raise RuntimeError("failed to schedule send task on event loop") + future = asyncio.wrap_future(task) try: send_result = future.result(timeout=60) except TimeoutError: