Search code examples
pythonpython-asynciotraceback

Is there a way to get the traceback of an async function which was called with asyncio.gather and see which function called it?


The problem can be summarised in this example:

import asyncio
import traceback

async def func_c():
    print("func c")
    traceback.print_stack()

async def func_b():
    print("func b")
    tasks = [func_c() for _ in range(3)]
    await asyncio.gather(*tasks)
    
async def func_b_v2():
    print("func b v2")
    tasks = [func_c() for _ in range(3)]
    await asyncio.gather(*tasks)

async def func_a():
    print("func a")
    await func_b()
    await func_b_v2()
    

async def main():
    await func_a()
    
if __name__ == "__main__":
    asyncio.run(main())

The problem I have is that traceback.print_stack() produces identical results if it were called by either func_b or func_b_v2 in their await asyncio.gather call.

The traceback looks something like:

File "test.py", line 28, in <module>
  asyncio.run(main())
File "asyncio/runners.py", line 190, in run
  return runner.run(main)
File "asyncio/runners.py", line 118, in run
  return self._loop.run_until_complete(task)
File "asyncio/base_events.py", line 640, in run_until_complete
  self.run_forever()
File "asyncio/base_events.py", line 607, in run_forever
  self._run_once()
File "asyncio/base_events.py", line 1919, in _run_once
  handle._run()
File "asyncio/events.py", line 80, in _run
  self._context.run(self._callback, *self._args)
File "test.py", line 6, in func_c
  traceback.print_stack()

Passing the traceback from function to function manually is not ideal because the func_c in my codebase is effectively used to log a result, but then be able to track the traceback of that result.

I have tried using other ways of calling multiple tasks concurrently e.g. using asyncio.create_task and using a for loop to await these tasks. This produces a similar result.

Similar issues did not help: Python traceback for coroutine (does not use the asyncio.gather function)


Solution

  • I'm afraid no. The coroutine was not called in the usual sense. Instead a task was created and the scheduler ran it.

    A function returns to the caller and the return address is stored in the stack, that's why a stack traceback shows this relationship. OTOH a task does not return to the caller. In general a task could be created anywhere in an async program and the created task can be awaited also anywhere, even at several places. There is no link with the desired information on the stack.

    As a workaround (kind of), you could give a name to a task (or give it a specific context).

    async def func_b():
        print("func b")
        tasks = [
            asyncio.create_task(func_c(), name="created in func_b")
            for _ in range(3)]
        await asyncio.gather(*tasks)
    

    To get the name given to the task:

    taskname = asyncio.current_task().get_name()
    print(taskname)