From 7aae52d18dbcde028ae671f74d9b18019dcd9d15 Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Tue, 23 Dec 2025 14:41:36 -0500 Subject: [PATCH] Bump pydocket to 0.16.2 for task cancellation support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a test that verifies task cancellation actually interrupts running coroutines (they receive CancelledError) rather than just marking the task as cancelled in Redis while the coroutine continues to completion. This requires pydocket >= 0.16.2 which added best-effort cancellation via Redis pub/sub signaling to workers. Closes #2679 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- pyproject.toml | 2 +- tests/server/tasks/test_task_methods.py | 43 +++++++++++++++++++++++++ uv.lock | 10 +++--- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5d3e1cda4..b480dc7d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "mcp>=1.24.0", "openapi-pydantic>=0.5.1", "platformdirs>=4.0.0", - "pydocket>=0.16.0", + "pydocket>=0.16.2", "rich>=13.9.4", "cyclopts>=4.0.0", "authlib>=1.6.5", diff --git a/tests/server/tasks/test_task_methods.py b/tests/server/tasks/test_task_methods.py index ba8b6e64d..ee971b2e7 100644 --- a/tests/server/tasks/test_task_methods.py +++ b/tests/server/tasks/test_task_methods.py @@ -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" + ) diff --git a/uv.lock b/uv.lock index 08c5f65c3..c8cae8b3a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", @@ -752,7 +752,7 @@ requires-dist = [ { name = "platformdirs", specifier = ">=4.0.0" }, { name = "py-key-value-aio", extras = ["disk", "keyring", "memory"], specifier = ">=0.3.0,<0.4.0" }, { name = "pydantic", extras = ["email"], specifier = ">=2.11.7" }, - { name = "pydocket", specifier = ">=0.16.0" }, + { name = "pydocket", specifier = ">=0.16.2" }, { name = "pyperclip", specifier = ">=1.9.0" }, { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "rich", specifier = ">=13.9.4" }, @@ -1775,7 +1775,7 @@ wheels = [ [[package]] name = "pydocket" -version = "0.16.0" +version = "0.16.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, @@ -1792,9 +1792,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/d3/ff57fb8ba8180c621b17270a272ee8821a64962887c42962c5a342ab82a9/pydocket-0.16.0.tar.gz", hash = "sha256:675e829a7ea6e978fdbbe0ace342f24c8ec7cf8ed5980694215418e59ff86005", size = 286316, upload-time = "2025-12-18T15:14:53.139Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b6/b9b5af725be7ffb195750e1cbe0ad4aa413858f16af258a49ad84cff82ac/pydocket-0.16.2.tar.gz", hash = "sha256:b48e59ad247cbf23fd7064cd765b75e5b870451a7778afa3b208b3aae6474c1f", size = 295623, upload-time = "2025-12-23T19:24:46.751Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/74/dcf56132e03a24e2591c5f2391902c871e5f1248e0fc175b9f1792e4afaf/pydocket-0.16.0-py3-none-any.whl", hash = "sha256:3d7d0a805bcb68a3a03d96d3c6ba8e607584ed14b0fe3879333a8fff652dda92", size = 61064, upload-time = "2025-12-18T15:14:51.506Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/81132e8ca41e11000807cc1f457a391d5a364dd1eeb7de40f2ff971301bc/pydocket-0.16.2-py3-none-any.whl", hash = "sha256:044a74deec2062c305bf1d452a1348a4e3739b3a2cf0a411ca1e11368cef5baf", size = 66812, upload-time = "2025-12-23T19:24:45.643Z" }, ] [[package]]