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