Bump pydocket to 0.16.3 to fix worker cleanup race condition

pydocket 0.16.3 fixes a race condition in `_worker_loop` where cancellation
arriving between `_worker_done.clear()` and the try block would cause
`_worker_done.set()` to never run, blocking `Worker.__aexit__` forever.

Also fixes:
- Simplified `_docket_lifespan` cleanup (timeout wrapper no longer needed)
- Fixed `nested_server` test fixture to use graceful uvicorn shutdown
- Fixed uv transport tests to use local fastmcp in dev mode

Closes #2679

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Guidry 2025-12-23 18:40:07 -05:00
commit 34112a17df
6 changed files with 215 additions and 153 deletions

View file

@ -172,3 +172,46 @@ async def test_task_cancellation_workflow(endpoint_server):
# Task should be in cancelled state
status = await task.status()
assert status.status == "cancelled"
async def test_task_cancellation_interrupts_running_coroutine(endpoint_server):
"""Task cancellation actually interrupts the running coroutine.
This verifies that when a task is cancelled, the underlying asyncio
coroutine receives CancelledError rather than continuing to completion.
Requires pydocket >= 0.16.2.
See: https://github.com/jlowin/fastmcp/issues/2679
"""
started = asyncio.Event()
was_interrupted = asyncio.Event()
completed_normally = asyncio.Event()
@endpoint_server.tool(task=True)
async def interruptible_tool() -> str:
started.set()
try:
await asyncio.sleep(60)
completed_normally.set()
return "completed"
except asyncio.CancelledError:
was_interrupted.set()
raise
async with Client(endpoint_server) as client:
task = await client.call_tool("interruptible_tool", {}, task=True)
# Wait for the tool to actually start executing
await asyncio.wait_for(started.wait(), timeout=5.0)
# Cancel the task
await task.cancel()
# Wait for cancellation to propagate
await asyncio.wait_for(was_interrupted.wait(), timeout=5.0)
# The coroutine should have been interrupted, not completed normally
assert was_interrupted.is_set(), "Task was not interrupted by cancellation"
assert not completed_normally.is_set(), (
"Task completed instead of being cancelled"
)