fix(weixin): asyncio.wait_for timeout in _api_post/_api_get (+ regression tests, cluster closure) - #35117
Merged
teknium1 merged 3 commits intoJun 2, 2026
Conversation
Collaborator
|
This PR supersedes #31853 (by @caojiguang) — carries the same production fix with preserved authorship, adds regression tests, and resolves the broader Weixin asyncio-timeout cluster (#29037, #24248, #19050, #17911). Also acknowledges independent work in #32179 (by @ryan-flow, closed). |
…api_post/_api_get Cron delivery to WeChat fails with 'Timeout context manager should be used inside a task' because _api_post and _api_get use aiohttp's ClientTimeout directly. When the cron scheduler calls send() via asyncio.run_coroutine_threadsafe(), aiohttp cannot find a running task and raises RuntimeError. _upload_media, _download_bytes, and _download_remote_media already use asyncio.wait_for() to avoid this. Apply the same pattern to _api_post and _api_get — the two remaining iLink API helpers that still use the raw ClientTimeout approach. This fixes cron delivery errors seen on the WeChat platform adapter when meyo-external cron jobs attempt to deliver output to WeChat.
The fix commit preserves @caojiguang's authorship (from NousResearch#31853); the release-notes AUTHOR_MAP gate requires their email to map to a GitHub username.
banditburai
force-pushed
the
worktree-fix+weixin-asyncio-loops
branch
from
May 30, 2026 15:53
2171b8e to
9a4fe2e
Compare
1 task
13 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Cron and other non-gateway WeChat sends fail with:
send_messageruns_api_post/_api_getin a worker thread with its own event loop (via_run_async), while the aiohttp session is bound to the gateway loop.aiohttp.ClientTimeoutis implemented throughasync_timeout.TimerContext, whose__enter__callsasyncio.current_task(loop=session._loop)— the gateway loop, not the worker loop — which returnsNoneand raises. The request never reaches the wire. The long-poll path was unaffected (already inside a Task), which is why this only bit cron / out-of-band sends.Fix
Replace the per-request
aiohttp.ClientTimeoutwithasyncio.wait_for(coro, timeout=timeout_ms/1000), wrapping each request body in a local coroutine.wait_forensure_future-wraps the coroutine into a real Task and enforces the deadline vialoop.call_later+fut.cancel()— it never callscurrent_task(), so it is event-loop-context-agnostic.This completes the migration begun in #21196, which moved the sibling helpers
_upload_ciphertext/_download_bytes/_download_remote_mediato the samewait_forpattern;_api_post/_api_getwere the last two helpers still on per-requestClientTimeout. All five Weixin HTTP helpers now use one timeout mechanism.Supporting context (unchanged, already on main): the send session is built with
aiohttp.ClientTimeout(total=None, connect=None, sock_connect=None, sock_read=None), so the session-level aiohttp timer is fully disabled and all timeout budget is owned by the per-requestwait_for.Scope / non-goals
Two files:
gateway/platforms/weixin.py,tests/gateway/test_weixin.py. This PR does not touch:model_tools.pyloop handling — the persistent/recreated-loop residuals ([Bug]: model_tools async bridge recreates loops in running-loop contexts #16570, Bug: persistent event loops in model_tools.py never closed, resource accumulation in long-running gateway #8043) live there and are out of scope.Behavior / compatibility
ClientTimeoutalready bounded these calls by the sametimeout_ms(API_TIMEOUT_MS=15s,CONFIG_TIMEOUT_MS=10s,LONG_POLL_TIMEOUT_MS=35s);wait_forenforces the identical budget. No truncation of previously-successful calls.wait_forraisesasyncio.TimeoutError; on Python 3.12 this isTimeoutErrorand is not aCancelledErrorsubclass, so the poll loop'sexcept CancelledErrordoes not swallow it — real sends retry then re-raise._get_updateslong-poll swallow is unchanged: it still catchesasyncio.TimeoutErrorand returns the empty-batch sentinel{"ret": 0, "msgs": [], "get_updates_buf": sync_buf}.wait_forcancels the inner coroutine mid-async with session.post(...); aiohttp's response context manager releases the connection in__aexit__— no leaked connection. Same pattern as the production sibling helpers.Tests
TestWeixinApiTimeout(7 cases):_api_post/_api_gettimeout=kwarg forwarded to the session call (regression guard against reintroducingClientTimeout)_api_post/_api_getwait_foractually fires and raisesasyncio.TimeoutError(real timeout, not mocked)_api_post/_api_getRuntimeErrorcarrying HTTP status + truncated body_get_updatesasyncio.TimeoutErroris swallowed into the empty-batch sentinel (loop survives)Note: tests pin the observable contract (no
timeout=forwarded;wait_forraises;_get_updatesswallows) rather than reproducing the loop-context crash in-process, which requires real aiohttp request machinery and is impractical to unit-test.Cluster closure
Resolves the Weixin asyncio-timeout cluster (15 active items). The eight issues below all report the timeout-context crash on the text-send path this fix removes:
Referenced, not closed by this diff:
Refs #13365, #23371, #18437— the cross-loop "Future attached to a different loop" half (session reuse across event loops); already fixed on main by fix(weixin): send_weixin_direct cross-loop session check #19141, not this diff.Refs #17595— media/file sends failing via cross-loop session reuse (fix(weixin): send_weixin_direct cross-loop session check #19141) plus the media-handling half (fix(gateway): unify MEDIA: extraction extension set + close the unknown-ext black hole (#34517) #34844); not this diff.Refs #32179— @ryan-flow's independent duplicate (closed as a duplicate of fix(weixin): replace aiohttp ClientTimeout with asyncio.wait_for in _api_post/_api_get #31853).Supersedes:
Supersedes #18714— its WeChat event-loop guard and Telegram proxy are already on main in stronger form (fix(weixin): send_weixin_direct cross-loop session check #19141 + the host-scoped Telegram proxy); only its out-of-cluster rate-limit backoff is unaddressed here.Supersedes #31853— carries @caojiguang's commit; close as incorporated once this merges.Known residuals / out of scope: #16570, #8043 — their timeout paths live in
model_tools.py, untouched by this diff.Infographic