8000 Create and Close multiple client session result "RuntimeError: Attempted to exit a cancel scope that isn't the current tasks's current cancel scope" · Issue #922 · modelcontextprotocol/python-sdk · GitHub
[go: up one dir, main page]

Skip to content
Create and Close multiple client session result "RuntimeError: Attempted to exit a cancel scope that isn't the current tasks's current cancel scope" #922
Open
@xtang2010

Description

@xtang2010

This is originally found in issue#788, but after some debugging, I believe this related to how ClientSession is implemented.

import contextlib
async def sessions():

    context1 = contextlib.AsyncExitStack()
    client1 = stdio_client(server_params_list['filesystem'])
    read1, write1 = await context1.enter_async_context(client1)
    session1 = await context1.enter_async_context(ClientSession(read1, write1))
    await session1.initialize()
    tools1 = await session1.list_tools()
    print(f"Session1 has {len(tools1.tools)} tools")

    context2 = contextlib.AsyncExitStack()
    client2 = sse_client(url = server_params_list['excel'].url)
    read2, write2 = await context2.enter_async_context(client2)
    session2 = await context2.enter_async_context(ClientSession(read2, write2))
    await session2.initialize()
    tools2 = await session2.list_tools()
    print(f"Session2 has {len(tools2.tools)} tools")

    await context1.aclose()
    await context2.aclose()

This is actually the simplified way how ClientSessionGroup() did it. This code will except in "await context1.aclose()". Please note if change the order:

    await context2.aclose()
    await context1.aclose()

This will go through without exception.

After some further looking, the ClientSession is use BaseSession, iwhere n its aenter(), BaseSession created a Task Group (to run receive_loop), and try to aexit() the task group during BaseSession's aexit()

The Task Group, among creation, will bind a "cancel scope" with "current_task". So when second ClientSession created, a new "cancel scope 2" is binding the "current_task", replace the first one. When you try to tear down the session1 now, evantually it try to call task_group.aexit() which lead to CancelScope().exit(), there it find the current_task() is binding to a different cancel_scope, thus the runtime.

The code below, demostrate the task group issue (without any mcp releated code):

async def taskgroup():
    import anyio
    from contextlib import AsyncExitStack

    async def task1(id):
        print(f"Task {id} started")
        await anyio.sleep(1)
        print(f"Task {id} running...")

    tg1 = anyio.create_task_group()
    await tg1.__aenter__()
    tg1.start_soon(task1, 1)

    tg2 = anyio.create_task_group()
    await tg2.__aenter__()
    tg2.start_soon(task1, 2)

    await anyio.sleep(3) 
    await tg1.__aexit__(None, None, None)
    await tg2.__aexit__(None, None, None)

Maybe BaseSession could consider other way instead of use anyio.create_task_group()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0