Skip to content

fix: keep WeCom callback receive loop responsive - #9759

Open
LIghtJUNction wants to merge 2 commits into
masterfrom
fix/wecom-initial-response-timeout
Open

fix: keep WeCom callback receive loop responsive#9759
LIghtJUNction wants to merge 2 commits into
masterfrom
fix/wecom-initial-response-timeout

Conversation

@LIghtJUNction

@LIghtJUNction LIghtJUNction commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #9758.

The WeCom long-connection receive loop awaited the callback handler inline. When the configured initial response was sent from that handler, it waited for a command acknowledgement that only the blocked receive loop could consume, causing repeated 10-second timeouts.

Modifications / 改动点

  • Dispatch WeCom callback handlers as tracked background tasks so the WebSocket receive loop remains available for command acknowledgements.

  • Preserve callback exceptions in logs and release completed task references.

  • Add a regression test that sends a response command from a callback handler and verifies that its acknowledgement is processed without blocking.

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

uv run pytest -q tests/test_wecomai_long_connection.py
1 passed
uv run ruff check .
All checks passed!

uv run ruff format --check .
501 files already formatted

Checklist / 检查清单

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Prevent WeCom callback processing from blocking command acknowledgements and improve Anthropic HTTP client compatibility.

Bug Fixes:

  • Keep the WeCom long-connection receive loop responsive while callback handlers send commands requiring acknowledgements.
  • Support Anthropic client compatibility with providers exposing the newer httpx2 module reference.

Enhancements:

  • Track background callback tasks, log their failures, and release completed task references.

Tests:

  • Add regression coverage verifying that callback responses receive acknowledgements without blocking the WeCom connection.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
astrbot-docs 09f87bd Commit Preview URL

Branch Preview URL
Aug 21 2026, 04:42 AM

@LIghtJUNction
LIghtJUNction marked this pull request as ready for review August 21, 2026 05:48
Copilot AI lite review requested due to automatic review settings August 21, 2026 05:48
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Aug 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@dosubot dosubot Bot added the area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. label Aug 21, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Consider cancelling or awaiting any still-running _message_handler_tasks as part of the client shutdown/cleanup path so that background callbacks don’t leak or keep the event loop alive longer than intended.
  • The new regression test asserts directly on the private _message_handler_tasks attribute; relying on internal state here makes the test brittle, so it may be better to assert on externally observable behavior (e.g., that the handler completes) instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider cancelling or awaiting any still-running `_message_handler_tasks` as part of the client shutdown/cleanup path so that background callbacks don’t leak or keep the event loop alive longer than intended.
- The new regression test asserts directly on the private `_message_handler_tasks` attribute; relying on internal state here makes the test brittle, so it may be better to assert on externally observable behavior (e.g., that the handler completes) instead.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/wecom_ai_bot/wecomai_long_connection.py" line_range="162-164" />
<code_context>
+        if task.cancelled():
+            return
+        if exception := task.exception():
+            logger.error(
+                "[WecomAI][LongConn] 处理回调消息失败",
+                exc_info=exception,
+            )
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Logging exception via `exc_info=exception` is unlikely to behave as intended.

`exc_info` should be either `True` (to use the current exception) or a `(type, value, traceback)` tuple, not the exception instance. To log the traceback for this task error, either pass a proper `exc_info` tuple or call `logger.exception("[WecomAI][LongConn] 处理回调消息失败")` immediately in the except block so the current exception is captured correctly.
</issue_to_address>

### Comment 2
<location path="tests/test_wecomai_long_connection.py" line_range="30-37" />
<code_context>
+        assert sent is True
+        handler_finished.set()
+
+    client = WecomAIBotLongConnectionClient(
+        bot_id="bot-id",
+        secret="secret",
+        ws_url="wss://example.com",
+        heartbeat_interval=30,
+        message_handler=message_handler,
+    )
+    client._ws = AsyncMock(closed=False)
+
+    callback = json.dumps(
</code_context>
<issue_to_address>
**suggestion (testing):** Assert that a background message handler task is created before processing the acknowledgement to make the regression intent clearer.

Currently the test only checks the final state (ACK processed and `_message_handler_tasks` empty). Please also assert that a background task was created (e.g., `assert len(client._message_handler_tasks) == 1` after the callback is handled and before the ACK), so the test explicitly verifies that the handler runs in a separate task rather than inline.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +162 to +164
logger.error(
"[WecomAI][LongConn] 处理回调消息失败",
exc_info=exception,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Logging exception via exc_info=exception is unlikely to behave as intended.

exc_info should be either True (to use the current exception) or a (type, value, traceback) tuple, not the exception instance. To log the traceback for this task error, either pass a proper exc_info tuple or call logger.exception("[WecomAI][LongConn] 处理回调消息失败") immediately in the except block so the current exception is captured correctly.

Comment on lines +30 to +37
client = WecomAIBotLongConnectionClient(
bot_id="bot-id",
secret="secret",
ws_url="wss://example.com",
heartbeat_interval=30,
message_handler=message_handler,
)
client._ws = AsyncMock(closed=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Assert that a background message handler task is created before processing the acknowledgement to make the regression intent clearer.

Currently the test only checks the final state (ACK processed and _message_handler_tasks empty). Please also assert that a background task was created (e.g., assert len(client._message_handler_tasks) == 1 after the callback is handled and before the ACK), so the test explicitly verifies that the handler runs in a separate task rather than inline.

@LIghtJUNction

Copy link
Copy Markdown
Contributor Author

这类问题我之前用ruff扫出来过

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09f87bde77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +144 to +146
task = asyncio.create_task(self.message_handler(payload))
self._message_handler_tasks.add(task)
task.add_done_callback(self._on_message_handler_done)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel callback tasks before reconnecting

When the socket disconnects while a callback handler is awaiting send_command, this task survives _run_once: only the heartbeat is cancelled, and shutdown() does not cancel these tasks either. After its 10-second wait expires, the stale handler can retry using the newly assigned _ws, sending an old callback response over the replacement connection while retaining _command_lock and delaying current callbacks. Cancel and await the handler tasks when their originating connection ends and during shutdown.

Useful? React with 👍 / 👎.

Comment on lines +156 to +157
def _on_message_handler_done(self, task: asyncio.Task[None]) -> None:
"""Release a completed callback task and report its exception."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Inline the one-off task completion logic

This class-level helper is used by only one callback registration and performs a small discard/cancellation/exception check, so it meets neither the three-location reuse threshold nor the extreme-complexity exception required for extracting helpers. Keep this completion handling local to the scheduling block rather than adding a separate method.

AGENTS.md reference: AGENTS.md:L67-L73

Useful? React with 👍 / 👎.

Comment on lines +144 to +146
task = asyncio.create_task(self.message_handler(payload))
self._message_handler_tasks.add(task)
task.add_done_callback(self._on_message_handler_done)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound callback task creation

When callbacks arrive faster than handlers finish, this creates and strongly retains one task per incoming frame without any queue or concurrency limit. This is especially problematic when initial responses serialize behind _command_lock, because one command can spend roughly 40 seconds retrying while every subsequent callback adds another pending task, allowing a burst or sustained stream to grow memory without bound. Use a bounded callback queue or concurrency limit while keeping the socket reader independent for acknowledgements.

Useful? React with 👍 / 👎.

Comment on lines +163 to +164
"[WecomAI][LongConn] 处理回调消息失败",
exc_info=exception,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Translate the callback failure log to English

The newly added callback failure message is written in Chinese, so this change violates the repository requirement that all logs use English. Translate the message so operational output remains consistent with the documented convention.

AGENTS.md reference: AGENTS.md:L46-L50

Useful? React with 👍 / 👎.

@LIghtJUNction

LIghtJUNction commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

第一是保存了个引用防止被回收
第二是解除阻塞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 企业微信智能机器人初始响应文本非空时,异常触发“等待命令响应超时”

2 participants