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

@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

    bugSomething isn't working

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions