Skip to content

fix: run sync handlers via anyio.to_thread.run_sync for cancellation - #33

Closed
neubig wants to merge 3 commits into
browser-use:mainfrom
neubig:fix/anyio-cancelable-sync-handlers
Closed

neubig wants to merge 3 commits into
browser-use:mainfrom
neubig:fix/anyio-cancelable-sync-handlers

Conversation

@neubig

@neubig neubig commented Aug 23, 2026 •

Copy link
Copy Markdown

Why

Sync handlers ran directly in the event loop thread (handler(event)), which blocked the loop entirely. If a sync handler hung (e.g. a C-level call), the timeout never fired and no other events could be processed — the entire event bus was frozen.

What this changes

Sync handlers now run via anyio.to_thread.run_sync(handler, event, abandon_on_cancel=True):

  • The event loop stays free to enforce timeouts and process other events
  • Cancellation is delivered to the await point (anyio's mechanism)
  • The worker thread may still be alive (Python can't kill threads), but the bus is no longer blocked on it

Complement to #32

PR #32 (Option C) addresses the async handler path: when a handler can't be cancelled, abandon it with a warning instead of holding the global lock. This PR (Option A) addresses the sync handler path: run sync handlers in a worker thread so the timeout can actually fire. Both can be merged independently.

Fixes #31.


Summary by cubic

Run synchronous handlers with anyio.to_thread.run_sync so handler timeouts take effect and the event loop remains responsive. Previously, sync handlers ran on the event loop thread, which froze the bus when a handler blocked and prevented timeouts from firing.

  • If a timeout is set, wrap the call in anyio.fail_after and await anyio.to_thread.run_sync(..., abandon_on_cancel=True); otherwise await run_sync without a deadline.
  • Cancellation abandons the await; the worker thread may continue, but the bus processes other events.
  • Adds tests/test_handler_timeout_zombie_thread.py to verify a blocking sync handler no longer blocks subsequent events after timeout.
  • Fixes Handler timeout leaves zombie threads (can't cancel blocking sync code) #31.

Written for commit 9c37fa9. Summary will update on new commits.

Review in cubic

neubig added 3 commits August 23, 2026 18:13
Failing test demonstrating that execute_handler's timeout fires but doesn't free threads blocked in synchronous code (run_in_executor). CancelledError can't be delivered to a blocking sync call, and the finally block waits only 0.1s before abandoning the task.
Sync handlers previously ran directly in the event loop thread, which
blocked the loop entirely and made the timeout useless. Now they run via
anyio.to_thread.run_sync(abandon_on_cancel=True), so:

- The event loop stays free to enforce timeouts
- Cancellation is delivered to the await point (anyio's mechanism)
- The worker thread may still be alive (Python can't kill threads), but
  the bus is no longer blocked on it

For async handlers that use loop.run_in_executor internally, users should
switch to anyio.to_thread.run_sync for the same cancellation benefit.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread bubus/service.py
Comment thread bubus/service.py
Comment thread tests/test_handler_timeout_zombie_thread.py
Comment thread tests/test_handler_timeout_zombie_thread.py
Comment thread tests/test_handler_timeout_zombie_thread.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handler timeout leaves zombie threads (can't cancel blocking sync code)

1 participant