aiohttp shouldn't invoke the exception handler for unexceptional situations

Created on 10 Nov 2020  ·  4Comments  ·  Source: aio-libs/aiohttp

🐞 Describe the bug
When certain objects from aiohttp get leaked, they invoke the asyncio exception handler, which in some environments (e.g. when using aiorun, with stop_on_unhandled_errors=True) means the entire program is stopped.

For example, here is the relevant code in ClientResponse:

https://github.com/aio-libs/aiohttp/blob/a8d9ec3f1667463e80545b1cacc7833d1ff305e9/aiohttp/client_reqrep.py#L748-L751

💡 To Reproduce
Leak an aiohttp object that does this.

💡 Expected behavior
Leaking objects should not cause an exception, for the same reason that open()ing a file and not .close()ing it does not raise an exception.

All objects that do this appear to already raise a warning about the resource leakage – I don't see a reason for them to invoke the exception handler as well.

📋 Your version of the Python

$ python --version
Python 3.8.6

📋 Your version of the aiohttp/yarl/multidict distributions

$ python -m pip show aiohttp
Name: aiohttp
Version: 3.6.2

```console
$ python -m pip show multidict
Name: multidict
Version: 4.7.6

```console
$ python -m pip show yarl
Name: yarl
Version: 1.5.1

📋 Additional context
cjrh/aiorun#56

bug wontfix

All 4 comments

I disagree.
The unclosed resource is a serious enough programming error that can be fixed easily by writing the correct code.
For example, asyncio itself uses call_exception_handler() for reporting about unawaited async calls.
If you don't want to write pedantic code -- you can use stop_on_unhandled_errors=False.

So, why not raise a real exception then?

I guess because raising an exception in __del__ will be ignored. But that's as it should be, no? It's very difficult to debug "something allocates an object and doesn't properly close it", especially when the bug is not in your own code but in some library; the source_traceback context key is undocumented, and it seems unfriendly to rely on undocumented behaviour of asyncio's default exception handler to make it possible to debug memory leaks.

I also don't see where asyncio uses call_exception_handler() to warn about unawaited coroutines. When an unawaited coroutine is garbage-collected, it invokes _PyErr_WarnUnawaitedCoroutine:

    /* If `gen` is a coroutine, and if it was never awaited on,
       issue a RuntimeWarning. */
    if (gen->gi_code != NULL &&
        ((PyCodeObject *)gen->gi_code)->co_flags & CO_COROUTINE &&
        gen->gi_frame->f_lasti == -1)
    {
        _PyErr_WarnUnawaitedCoroutine((PyObject *)gen);
    }

which invokes warnings._warn_unawaited_coroutine():

void
_PyErr_WarnUnawaitedCoroutine(PyObject *coro)
{
    /* First, we attempt to funnel the warning through
       warnings._warn_unawaited_coroutine.

which emits a RuntimeWarning. I don't see call_exception_handler() used anywhere there – in fact I don't see any use of call_exception_handler() in stdlib that doesn't include exception in the context.

And again: if forgetting to close a file doesn't raise an exception, why should forgetting to clean up whatever random aiohttp object?

You are right, raising an exception from __del__ method is a discouraged practice.
That's why we need another signaling solution.
Asyncio does call call_exception_handler() from __del__, see Task.__del__ for example: https://github.com/python/cpython/blob/master/Lib/asyncio/tasks.py#L139-L148

You are right, source_traceback is not documented in https://docs.python.org/3/library/asyncio-eventloop.html?highlight=call_exception_handler#asyncio.loop.call_exception_handler
Would you mind create a pull request on https://github.com/python/cpython/ to fix the documentation? I happy to review/merge it.

And again: if forgetting to close a file doesn't raise an exception, why should forgetting to clean up whatever random aiohttp object?

aiohttp doesn't raise an exception from __del__ too, it is just impossible in Python because the object finalizer's call is indeterministic.

Unclosed file raises a ResourceWarning. This warning is ignored by default: https://docs.python.org/3/library/warnings.html#default-warning-filter
That's why this kind of warnings is not visible for very many developers if they don't enable them explicitly. This is done partially for historical reasons: for a long time this kind of warning did not exist, many libraries don't call file.close() because their authors are not aware of the need for graceful resource cleanup, etc., etc.

Fortunately, file.close() could be called from file.__del__ safely, it is a regular call.
But in asyncio world close() is different very often: it has to be an async function which just cannot be called from __del__.
For example, transport.close() without waiting for protocol.connection_lost() leads to warnings even for plain sockets; for SSL the situation is even worse.
aiohttp has a little mess with sync/async finalizers but I'm working on it; aiohttp 4.0 should be cleaned up significantly.

That's why the cleanup is very important, a warning should be raised, and call_exception_handler() will be used to make the bad usage more visible.

Asyncio _does_ call call_exception_handler() from __del__, see Task.__del__ for example

Fair enough, I missed that call.

Would you mind create a pull request on python/cpython to fix the documentation?

I'm honestly unsure how best to document it. I created bpo-42347 since I noticed some other discrepancies between the docs, the docstring, and the implementation.

That's why the cleanup is very important, a warning should be raised, and call_exception_handler() will be used to make the bad usage more visible.

I guess, it's just frustrating when you get errors because a library you're using doesn't clean up after itself properly. I didn't realise it was so important it should stop the application, and I didn't initially even realise that the leak was the reason aiorun was stopping the loop.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

alxpy picture alxpy  ·  5Comments

ahuigo picture ahuigo  ·  5Comments

ZeusFSX picture ZeusFSX  ·  5Comments

AtomsForPeace picture AtomsForPeace  ·  5Comments

JulienPalard picture JulienPalard  ·  3Comments