Skip to content

Weirdness with traceback when resuming coroutines with coro.throw() #93592

@kristjanvalur

Description

@kristjanvalur

Bug report

When a coroutine is resumed using coro.throw(), e.g. by using Task.cancel(), the frame.f_back chain becomes
mysteriously truncated at arbitrary places in the call stack.

Your environment

  • CPython versions tested on: 3.10.4
  • Operating system and architecture: Win 11

The following example pytest code demonstrates the problem. A recursive stack of coroutines is resumed, either normally (after a sleep) or by sending a CancelledError in. In the latter case, a traceback is generated and it is, in all cases, curiously truncated after just a few steps. Manually walking the stack will result in a f.f_back == None after a few steps.

I employ a few different recursions, including a manual await-like method operating directly on the coroutine protocol. Initially I suspected that coro.throw() was deliberatly engineeded to mess with the f.f_back of the frame chain, but even call stacks using regular coroutine recursion (the await keyword on async functions) appear truncated.

traceback.print_stack() is ultimately doing a f = sys._getframe() and following the f = f.f_back chain. It can just as easily be verified that this chain is broken with a None after a short bit.

import asyncio
import pytest
import types
import traceback


@types.coroutine
def myawait(coro):
    try:
        val = coro.send(None)
    except StopIteration as e:
        return e.value
    while True:
        try:
            val = yield val
        except BaseException as e:
            try:
                val = coro.throw(e)
            except StopIteration as e:
                return e.value
        else:
            try:
                val = coro.send(val)
            except StopIteration as e:
                return e.value

async def realawait(coro):
    return await coro

async def bas(result):
    return await bar(result)

async def foo1(result, n=2):
    if n:
        return await foo1(result, n-1)
    return await bas(result)

async def foo2(result, n=2):
    if n:
        return await foo2(result, n-1)
    return await realawait(bar(result))

async def foo3(result, n=2):
    if n:
        return await foo3(result, n-1)
    return await myawait(bar(result))

async def bar(result):
    try:
        await asyncio.sleep(0.1)
    except asyncio.CancelledError:
        traceback.print_stack(limit=5)
        result.append(False)
        result.append(traceback.format_stack())
    else:
        traceback.print_stack(limit=5)
        result.append(True)
        result.append(traceback.format_stack())
   
@pytest.mark.parametrize("func", [foo1, foo2, foo3])
async def test_regular(func):
    result = []
    t = asyncio.Task(func(result))
    await asyncio.sleep(0)
    await t
    ok, stack = result
    assert ok
    assert len(stack) > 5

@pytest.mark.xfail()
@pytest.mark.parametrize("func", [foo1, foo2, foo3])
async def test_truncated(func):
    result = []
    t = asyncio.Task(func(result))
    await asyncio.sleep(0)
    t.cancel()
    await t
    ok, stack = result
    assert not ok
    assert len(stack) > 5

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions