Skip to content

test: demonstrate cancellation can't free blocking sync code (2 failing tests) - #4599

Open
neubig wants to merge 1 commit into
mainfrom
fix/cancellation-deadlock-tests
Open

test: demonstrate cancellation can't free blocking sync code (2 failing tests)#4599
neubig wants to merge 1 commit into
mainfrom
fix/cancellation-deadlock-tests

Conversation

@neubig

@neubig neubig commented Aug 23, 2026

Copy link
Copy Markdown
Member

Why

Two failing tests that demonstrate the same underlying problem: asyncio cancellation (CancelledError) cannot be delivered to tasks blocked in synchronous code (C-level locks, blocking I/O, futexes). The cleanup code tries to cancel but gives up after a tiny timeout, leaving zombie threads that accumulate until the process is killed.

Tests

Test 1: test_async_executor_close_hangs_on_blocking_task

Demonstrates that AsyncExecutor.close() hangs forever when a task is blocked in a worker thread. Without the fix, close() waits indefinitely for the task to finish on its own.

Result on main: FAILS after 5s — close() hangs.

AssertionError: AsyncExecutor.close() hung >5s on a task blocked in a worker thread.

Addresses #4597 (duplicate of #4546, with a cleaner test).

Test 2: test_bubus_timeout_does_not_free_blocked_handler_thread

Demonstrates that bubus EventBus handler timeout fires after 1s and logs the error, but the handler's worker thread (via run_in_executor) stays alive forever. The finally block waits only 0.1s for cancellation to take effect, then abandons the task.

Result on main: FAILS — 1 zombie thread remains.

AssertionError: bubus timeout fired but 1 thread(s) are still alive.

Addresses #4598.

Production impact

Both bugs manifested on a self-hosted agent-server: conversations that used the browser tool accumulated zombie threads (21 futex-stuck threads across 11 conversations) until the thread pool was exhausted and new conversation creation stalled. The server looked healthy (/health returned 200) but was wedged.

Run

uv run pytest tests/sdk/utils/test_cancellation_deadlock.py -xvs

Both tests should fail on main. They should pass once #4597 and #4598 are fixed.


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:4be3a8f-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-4be3a8f-python \
  ghcr.io/openhands/agent-server:4be3a8f-python

All tags pushed for this build

ghcr.io/openhands/agent-server:4be3a8f-golang-amd64
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-golang-amd64
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-golang-amd64
ghcr.io/openhands/agent-server:4be3a8f-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:4be3a8f-golang-arm64
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-golang-arm64
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-golang-arm64
ghcr.io/openhands/agent-server:4be3a8f-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:4be3a8f-java-amd64
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-java-amd64
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-java-amd64
ghcr.io/openhands/agent-server:4be3a8f-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:4be3a8f-java-arm64
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-java-arm64
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-java-arm64
ghcr.io/openhands/agent-server:4be3a8f-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:4be3a8f-python-amd64
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-python-amd64
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-python-amd64
ghcr.io/openhands/agent-server:4be3a8f-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:4be3a8f-python-arm64
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-python-arm64
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-python-arm64
ghcr.io/openhands/agent-server:4be3a8f-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:4be3a8f-golang
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-golang
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-golang
ghcr.io/openhands/agent-server:4be3a8f-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:4be3a8f-java
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-java
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-java
ghcr.io/openhands/agent-server:4be3a8f-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:4be3a8f-python
ghcr.io/openhands/agent-server:4be3a8f6d5c7733d4794e16377b469404f116192-python
ghcr.io/openhands/agent-server:fix-cancellation-deadlock-tests-python
ghcr.io/openhands/agent-server:4be3a8f-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 4be3a8f-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 4be3a8f-python-amd64) are also available if needed

Two failing tests showing the same root cause: asyncio cancellation
(CancelledError) cannot be delivered to tasks blocked in synchronous code
(C-level locks, blocking I/O, futexes). The cleanup code tries to cancel
but gives up after a tiny timeout, leaving zombie threads.

Test 1: AsyncExecutor.close() hangs forever when a task is blocked in a
worker thread (anyio.to_thread). close() waits indefinitely for the task
to finish on its own.

Test 2: bubus EventBus handler timeout fires after 1s and logs the error,
but the handler's worker thread (via run_in_executor) stays alive forever
because cancelling the asyncio Future doesn't cancel the underlying thread.
The finally block waits only 0.1s then abandons the task.

In production, both accumulate zombie threads until the thread pool is
exhausted and new conversation creation stalls.
@github-actions

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@all-hands-bot

Copy link
Copy Markdown
Collaborator

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

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.

3 participants