mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-28 02:10:38 +02:00
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:
parent
5daabf0b84
commit
34112a17df
6 changed files with 215 additions and 153 deletions
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue