question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

I get "anyio._backends._asyncio.ExceptionGroup: 0 exceptions were raised in the task group"

See original GitHub issue

The following code reproduces the issue. Sorry about the length of it.

from __future__ import annotations

from contextlib import suppress
from pathlib import Path
from subprocess import STDOUT, CalledProcessError
from typing import Optional, Sequence, Union

import anyio
from anyio.abc import Process
from anyio.streams.file import FileReadStream
from anyio.streams.text import TextReceiveStream


async def run_process(
    # tg: TaskGroup,
    command: Union[str, Sequence[str]],
    *,
    input_for_stdin: Optional[Path] = None,
    raise_on_rc: Optional[bool] = None,
) -> None:
    """Run the given command as a foreground process.

    Unlike `anyio.run_process`, this streams data to/from the process while the
    process runs. This way, you can see the process' output while it's running.
    Useful for long-running processes.
    """
    process: Optional[Process] = None
    try:
        process = await anyio.open_process(command, stderr=STDOUT)
        await drain_streams(process, input_for_stdin)
    except BaseException:
        if process is not None:
            # Try to gracefully terminate the process
            process.terminate()
            # Give the process some time to stop
            with anyio.move_on_after(5, shield=True):
                await drain_streams(process)
        raise
    finally:
        if process is not None:
            # We tried to be graceful. Now there is no mercy.
            with suppress(ProcessLookupError):
                process.kill()
            # Close the streams (stdin, stdout, stderr)
            await process.aclose()

    assert process.returncode is not None
    # Check the return code (rc)
    if raise_on_rc and process.returncode != 0:
        raise CalledProcessError(process.returncode, command)


async def drain_streams(
    process: Process, input_for_stdin: Optional[Path] = None
) -> None:
    async with anyio.create_task_group() as tg:
        # In parallel:
        #  * send to stdin
        #  * receive from stdout
        if process.stdin is not None and input_for_stdin is not None:
            tg.start_soon(_send_to_stdin, process, input_for_stdin)
        if process.stdout is not None:
            tg.start_soon(_receive_from_stdout, process)
        # Wait for normal exit
        await process.wait()


async def _send_to_stdin(process: Process, input_for_stdin: Path) -> None:
    assert process.stdin is not None
    # Forward data from file to stdin
    async with await FileReadStream.from_path(input_for_stdin) as chunks:
        async for chunk in chunks:
            await process.stdin.send(chunk)


async def _receive_from_stdout(process: Process) -> None:
    assert process.stdout is not None
    # Forward data from stdout
    async for string in TextReceiveStream(process.stdout):
        print(string)


async def main():
    async with anyio.create_task_group() as tg:
        # Run the process in the "background"
        tg.start_soon(run_process, ("sleep", "10"))
        # We can do something else while the process runs
        print("Sleeping now. Try to press CTRL+C.")
        await anyio.sleep(10)


anyio.run(main)

Try to press CTRL+C while it runs. Example stack trace:

Sleeping now. Try to press CTRL+C.
^Cunhandled exception during asyncio.run() shutdown
task: <Task finished name='__main__.run_process' coro=<run_process() done, defined at /projects/stork/anyio_bug.py:14> exception=<ExceptionGroup: >>
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 629, in run_until_complete
    self.run_forever()
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 596, in run_forever
    self._run_once()
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 1854, in _run_once
    event_list = self._selector.select(timeout)
  File "/usr/local/lib/python3.9/selectors.py", line 469, in select
    fd_event_list = self._selector.poll(timeout, max_ev)
KeyboardInterrupt

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/projects/stork/anyio_bug.py", line 65, in drain_streams
    await process.wait()
  File "/projects/stork/.venv/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 825, in wait
    return await self._process.wait()
  File "/usr/local/lib/python3.9/asyncio/subprocess.py", line 135, in wait
    return await self._transport._wait()
  File "/usr/local/lib/python3.9/asyncio/base_subprocess.py", line 235, in _wait
    return await waiter
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/projects/stork/anyio_bug.py", line 30, in run_process
    await drain_streams(process, input_for_stdin)
  File "/projects/stork/anyio_bug.py", line 65, in drain_streams
    await process.wait()
  File "/projects/stork/.venv/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 526, in __aexit__
    raise ExceptionGroup(exceptions)
anyio._backends._asyncio.ExceptionGroup: 0 exceptions were raised in the task group:
----------------------------

Traceback (most recent call last):
  File "/projects/stork/anyio_bug.py", line 92, in <module>
    anyio.run(main)
  File "/projects/stork/.venv/lib/python3.9/site-packages/anyio/_core/_eventloop.py", line 55, in run
    return asynclib.run(func, *args, **backend_options)  # type: ignore
  File "/projects/stork/.venv/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 211, in run
    return native_run(wrapper(), debug=debug)
  File "/usr/local/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 629, in run_until_complete
    self.run_forever()
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 596, in run_forever
    self._run_once()
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 1854, in _run_once
    event_list = self._selector.select(timeout)
  File "/usr/local/lib/python3.9/selectors.py", line 469, in select
    fd_event_list = self._selector.poll(timeout, max_ev)
KeyboardInterrupt

The interesting part to me is the anyio._backends._asyncio.ExceptionGroup: 0 exceptions were raised in the task group message. Why does anyio raise an ExceptionGroup with zero exceptions in it?

If I’m doing something that I’m not supposed to, let me know. 😃

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:1
  • Comments:19 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
graingertcommented, May 6, 2021

I think that sounds like a good and pragmatic solution.

Does this fix:

* avoid the whole `unhandled exception during asyncio.run() shutdown` situation to begin with?

yes, asyncio.run ignores CancelledError exceptions here https://github.com/python/cpython/blob/ce47addfb6f176fad053431b537b77a5f170765e/Lib/asyncio/runners.py#L67-L68

* simply change the stack trace from  `anyio._backends._asyncio.ExceptionGroup: 0 exceptions were raised in the task group` to `asyncio.exceptions.CancelledError`?

there won’t be a an ExceptionGroup as long as all the exceptions are CancelledError

I’d prefer the former but maybe that’s impossible with the asyncio backend.

1reaction
agronholmcommented, May 5, 2021

The line where the exceptions list is replaced can be found here: https://github.com/agronholm/anyio/blob/master/src/anyio/_backends/_asyncio.py#L559

It’s not clear if the logic for when to filter out CancelledError is correct. Perhaps we should be checking if the host task has been cancelled. Either way, I believe I can fix this one particular wart without any side effects.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Surprising behavior because of ExceptionGroup #141 - GitHub
Hi, Imagine the following code: import anyio async def task1(): # This task raises an exception. Oops. raise ValueError async def task2(): raise...
Read more >
API reference — AnyIO 3.6.2 documentation - Read the Docs
backend ( str ) – name of the asynchronous event loop implementation ... if the task completes successfully, or with the exception raised...
Read more >
Version history — AnyIO 3.6.2 documentation - Read the Docs
Fixed task parent ID not getting set to the correct value on asyncio ... Fixed odd ExceptionGroup: 0 exceptions were raised in the...
Read more >
Version history — AnyIO 1.4.0 documentation - Read the Docs
Fixed fail.after(0) not raising a timeout error on asyncio and curio ... errors leaking from a task group when they are contained in...
Read more >
AnyIO - Release 3.6.2 Alex Grönholm
A task group is an asynchronous context manager that makes sure that all its child tasks are finished one way or another after...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found