fix(session_waiter): track handler task to prevent execution after timeout - #9458
fix(session_waiter): track handler task to prevent execution after timeout#9458Qixuan112 wants to merge 4 commits into
Conversation
…meout - Add _handler_task attribute to SessionWaiter to track running handler tasks - Use asyncio.create_task to wrap handler execution for proper task management - Cancel any running handler task during cleanup to prevent orphaned execution - Cancel previous handler task if a new trigger arrives before completion - Handle asyncio.CancelledError explicitly to avoid error propagation This resolves the TODO at line 167 and ensures that handler tasks are properly managed and cancelled when sessions timeout or cleanup, preventing potential resource leaks and unexpected behavior from handlers that continue running after their session has ended.
📄 Knowledge reviewDosu skipped reviewing this PR because your organization has used its |
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- Consider clearing
self._handler_task(e.g., setting it toNoneonce the task is done) to avoid holding stale task references and make the task lifecycle easier to reason about. - In
_run_handler, exceptions are caught andsession.session_controller.stop(e)is called, but sinceawait session._handler_taskalso propagates exceptions to the outerexcept Exception,stopmay be invoked twice; you may want to centralize the error handling to a single place to avoid duplicate stop calls.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider clearing `self._handler_task` (e.g., setting it to `None` once the task is done) to avoid holding stale task references and make the task lifecycle easier to reason about.
- In `_run_handler`, exceptions are caught and `session.session_controller.stop(e)` is called, but since `await session._handler_task` also propagates exceptions to the outer `except Exception`, `stop` may be invoked twice; you may want to centralize the error handling to a single place to avoid duplicate stop calls.
## Individual Comments
### Comment 1
<location path="astrbot/core/utils/session_waiter.py" line_range="124-125" />
<code_context>
self._lock = asyncio.Lock()
"""需要保证一个 session 同时只有一个 trigger"""
+ self._handler_task: asyncio.Task | None = None
+ """当前正在运行的 handler 任务"""
+
async def register_wait(
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider how CancelledError is handled to avoid treating cancellations as generic errors.
With `_handler_task` cancellations now part of normal control flow (e.g., timeouts or new triggers), `CancelledError` is currently caught by the broad `except Exception` in `_run_handler` and sent to `session.session_controller.stop(e)`, incorrectly treating normal cancellations as errors. Please either re-raise `CancelledError` in `_run_handler` so the outer `except asyncio.CancelledError` can handle it, or narrow the inner `except` to exclude `CancelledError` so cancellations don’t trigger `stop` with an error.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
Fixed by re-raising CancelledError in _run_handler so cancellations are handled by the outer except block instead of being treated as errors. 🤖 Addressed by Claude Code |
…lations as errors
xiaoyuyu6420
left a comment
There was a problem hiding this comment.
Review
This PR addresses the TODO at line 217 — the original code await session.handler(...) inside _lock means a long-running handler blocks the lock, and after timeout cleanup the handler keeps running with no way to cancel it. Tracking the task is the right direction.
Strengths
- Waiting outside the lock (
task_to_awaitpattern) — correctly avoids holding_lockduring handler execution, which prevents deadlock when_cleanup()tries to acquire the same lock to cancel the task. - Re-raising
CancelledErrorinside_run_handler— correct, ensures the outer except clause can distinguish cancellation from other exceptions. - Cancelling previous task before creating new one — prevents overlapping handler execution if
trigger()is called rapidly.
Issues
1. _cleanup() is now async — this is a breaking change for any caller that invokes it synchronously
_cleanup() was a sync method. Making it async means any code that calls self._cleanup() without await will silently create a coroutine that never executes. Within this file both call sites are updated, but this is a public-ish method on SessionWaiter — third-party code or plugins that call _cleanup() directly will break silently.
2. Deadlock risk: _cleanup() acquires _lock, but trigger() also holds _lock when the handler runs
Consider this sequence:
trigger()acquires_lock, creates_handler_task, releases_lock, thenawait task_to_await- While handler is running, timeout fires →
register_wait()callsawait self._cleanup() _cleanup()triesasync with self._lockto cancel_handler_task- If the handler itself calls
controller.keep()(which doesn't touch_lock), this works. But if the handler somehow triggers anothertrigger()call on the same session (recursive),_lockwould already be released by then, so this is probably OK.
Actually, on closer inspection, since trigger() releases _lock before awaiting task_to_await, and _cleanup() acquires _lock only to cancel the task, there shouldn't be a deadlock. But the interaction is subtle enough to warrant a comment explaining why it's safe.
3. No test for the task cancellation behavior
The core value of this PR — cancelling a handler after timeout — has no test. A test should verify:
- When timeout fires while handler is running,
_handler_taskis cancelled - When
trigger()is called again before previous handler finishes, previous task is cancelled
4. _handler_task exception handling asymmetry
In _run_handler, exceptions are caught and passed to session.session_controller.stop(e). But in the outer await task_to_await, only CancelledError is caught — if the task raises any other exception (which shouldn't happen because _run_handler catches them all), it would propagate to trigger()'s caller. This is probably fine but the double-layer exception handling is confusing.
Summary
Good direction — the task tracking pattern correctly addresses the TODO. The main concerns are: (1) the async _cleanup() breaking change, (2) missing tests for the cancellation behavior, and (3) the subtle lock interaction deserves documentation. Consider adding a test that verifies a timed-out handler actually gets cancelled.
Add locked unit tests covering the task-tracking mechanism introduced in AstrBotDevs#9458, addressing review feedback that cancellation behavior was untested: - Basic trigger flow: handler runs and register_wait returns - Double trigger cancels the previous still-running handler task - _cleanup cancels a running handler task (timeout path) - CancelledError is NOT funneled into SessionController.stop() as an ordinary exception (cleanup and double-trigger paths) - Handler exceptions propagate to the future via stop(e) These tests encode the expected behavior and serve as the acceptance contract for future changes.
Respond to @xiaoyuyu6420 and sourcery review comments on AstrBotDevs#9458: - Document _cleanup as an internal async method (it must acquire _lock to safely cancel _handler_task); confirmed no external callers exist - Add comments explaining why awaiting the handler task outside the lock cannot deadlock with _cleanup - Add comments explaining the two-layer exception handling: inner layer converts handler exceptions to stop(e), outer layer silently handles task cancellation - Clear _handler_task reference after the task completes (guarded by an identity check to avoid clobbering a concurrent trigger's task) No control-flow changes; behavior is unchanged and all unit tests pass.
|
@xiaoyuyu6420 感谢详细审查。已逐项处理并推送: 1.
|
问题描述
在 第 167 行有一个 TODO 注释,指出需要使用 来跟踪任务,防止超时后 handler 仍然在执行。
解决方案
改进点
测试
相关 TODO
解决了 的 TODO 注释。
Summary by Sourcery
Track and manage session handler tasks to prevent handlers from running after session timeout.
Bug Fixes:
Enhancements: