Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asyncio stream doesn't handle exceptions in callback #110894

Closed
LasseBlaauwbroek opened this issue Oct 15, 2023 · 2 comments · Fixed by #111601
Closed

Asyncio stream doesn't handle exceptions in callback #110894

LasseBlaauwbroek opened this issue Oct 15, 2023 · 2 comments · Fixed by #111601
Assignees
Labels
3.11 only security fixes 3.12 bugs and security fixes 3.13 new features, bugs and security fixes topic-asyncio type-bug An unexpected behavior, bug, or error

Comments

@LasseBlaauwbroek
Copy link

LasseBlaauwbroek commented Oct 15, 2023

Bug report

Bug description:

Consider the following server that always just crashes:

import asyncio

async def handle_echo(reader, writer):
    raise Exception("I break everything")

async def main():
    server = await asyncio.start_server(
        handle_echo, '127.0.0.1', 8888)
    async with server:
        await server.serve_forever()

asyncio.run(main(), debug=True)

Together with the following client:

import asyncio

async def tcp_echo_client(message):
    reader, writer = await asyncio.open_connection(
        '127.0.0.1', 8888)

    data = await reader.read(100)
    print(data)
    writer.close()
    await writer.wait_closed()
    print("closed")

asyncio.run(tcp_echo_client('Hello World!'), debug=True)

When running this in Python 3.8, the server prints a Task exception was never retrieved message with an exception. I guess this is acceptable. However, the connection is never closed, and the client is kept hanging. That should not happen in my opinion.

Furthermore, in newer Python versions, due to #96323, the internal task is being kept alive indefinitely. Therefore, the Task exception was never retrieved message is only printed when you kill the server. Otherwise, the failure is completely silent. This is extremely counter-intuitive behavior.

CPython versions tested on:

3.11

Operating systems tested on:

Linux

Linked PRs

@LasseBlaauwbroek LasseBlaauwbroek added the type-bug An unexpected behavior, bug, or error label Oct 15, 2023
LasseBlaauwbroek added a commit to LasseBlaauwbroek/pycapnp that referenced this issue Oct 15, 2023
In its current form, when a server callback throws an exception, it is
completely swallowed. Only when the asyncio loop is being shut down might one
possibly see that error. On top of that, the connection is never closed, causing
any clients to hang, and a memory leak in the server.

This is a proposed fix that reports the exception to the asyncio exception
handler. It also makes sure that the connection is always closed, even if the
callback doesn't close it explicitly.

Note that the design of AsyncIoStream is directly based on the design of
Python's asyncio streams: https://docs.python.org/3/library/asyncio-stream.html
These streams appear to have exactly the same flaw. I've reported this here:
python/cpython#110894. Since I don't really know what
I'm doing, it might be worth seeing what kind of solution they might come up
with and model our solution after theirs.
@LasseBlaauwbroek
Copy link
Author

LasseBlaauwbroek commented Oct 15, 2023

Note that I found out about this problem because I created a stream-like asyncio system for PyCapnp. It seemed smart to base it off the Python stream design, but now it suffers from exactly the same problem :-)

My proposed solution for PyCapnp is here: capnproto/pycapnp#337. I'd imagine that something similar is needed here.

haata pushed a commit to capnproto/pycapnp that referenced this issue Oct 23, 2023
In its current form, when a server callback throws an exception, it is
completely swallowed. Only when the asyncio loop is being shut down might one
possibly see that error. On top of that, the connection is never closed, causing
any clients to hang, and a memory leak in the server.

This is a proposed fix that reports the exception to the asyncio exception
handler. It also makes sure that the connection is always closed, even if the
callback doesn't close it explicitly.

Note that the design of AsyncIoStream is directly based on the design of
Python's asyncio streams: https://docs.python.org/3/library/asyncio-stream.html
These streams appear to have exactly the same flaw. I've reported this here:
python/cpython#110894. Since I don't really know what
I'm doing, it might be worth seeing what kind of solution they might come up
with and model our solution after theirs.
@kumaraditya303
Copy link
Contributor

I have a fix for this as #111601, we call loop handler so that applications get a chance to handle it

@kumaraditya303 kumaraditya303 self-assigned this Nov 2, 2023
@kumaraditya303 kumaraditya303 added 3.11 only security fixes 3.12 bugs and security fixes 3.13 new features, bugs and security fixes labels Nov 2, 2023
kumaraditya303 added a commit that referenced this issue Nov 2, 2023
…cted_cb (#111601)

Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it.
kumaraditya303 added a commit to kumaraditya303/cpython that referenced this issue Nov 2, 2023
… client_connected_cb (pythonGH-111601)

Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it..
(cherry picked from commit 229f44d)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
kumaraditya303 added a commit to kumaraditya303/cpython that referenced this issue Nov 2, 2023
… client_connected_cb (pythonGH-111601)

Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it..
(cherry picked from commit 229f44d)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
kumaraditya303 added a commit that referenced this issue Nov 2, 2023
…t_connected_cb (GH-111601) (#111632)

Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it..
(cherry picked from commit 229f44d)
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Nov 2, 2023
… client_connected_cb (pythonGH-111601) (pythonGH-111632)

(cherry picked from commit 9aa8829)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it..
(cherry picked from commit 229f44d)
FullteaR pushed a commit to FullteaR/cpython that referenced this issue Nov 3, 2023
…_connected_cb (python#111601)

Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it.
vstinner added a commit that referenced this issue Nov 4, 2023
…n client_connected_cb (GH-111601) (GH-111632) (#111634)

* [3.12] GH-110894: Call loop exception handler for exceptions in client_connected_cb (GH-111601) (GH-111632)
(cherry picked from commit 9aa8829)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it..
(cherry picked from commit 229f44d)

* gh-111644: Fix asyncio test_unhandled_exceptions() (#111713)

Fix test_unhandled_exceptions() of test_asyncio.test_streams: break
explicitly a reference cycle.

Fix also StreamTests.tearDown(): the loop must not be closed
explicitly, but using set_event_loop() which takes care of shutting
down the executor with executor.shutdown(wait=True).
BaseEventLoop.close() calls executor.shutdown(wait=False).

(cherry picked from commit ac01e22)

---------

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Co-authored-by: Victor Stinner <vstinner@python.org>
aisk pushed a commit to aisk/cpython that referenced this issue Feb 11, 2024
…_connected_cb (python#111601)

Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.11 only security fixes 3.12 bugs and security fixes 3.13 new features, bugs and security fixes topic-asyncio type-bug An unexpected behavior, bug, or error
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

3 participants