pytest-asyncio icon indicating copy to clipboard operation
pytest-asyncio copied to clipboard

ResourceWarning: unclosed event loop

Open graingert opened this issue 2 years ago • 16 comments

using pytest-asyncio asyncio 0.23.2

import pytest

import pytest_asyncio


@pytest_asyncio.fixture()
async def demo():
    yield


@pytest.mark.asyncio
async def test_aiohttp_test_client_json(demo):
    pass


def test_redirect():

    import asyncio
    async def amain():
        pass
    asyncio.run(amain())

    import gc
    gc.collect()

results in

REQUESTS_CA_BUNDLE=/home/graingert/projects/vcrpy/.tox/py311-aiohttp/lib/python3.11/site-packages/pytest_httpbin/certs/cacert.pem ./.tox/py311-aiohttp/bin/pytest tests/integration/test_aiohttp.py -s
================================================================================ test session starts =================================================================================
platform linux -- Python 3.11.7+, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/graingert/projects/vcrpy
configfile: pyproject.toml
plugins: aiohttp-1.0.5, httpbin-2.0.0, cov-4.1.0, asyncio-0.23.2
asyncio: mode=Mode.STRICT
collected 2 items                                                                                                                                                                    

tests/integration/test_aiohttp.py ..

================================================================================== warnings summary ==================================================================================
tests/integration/test_aiohttp.py::test_redirect
  /usr/lib/python3.11/asyncio/base_events.py:692: ResourceWarning: unclosed event loop <_UnixSelectorEventLoop running=False closed=False debug=False>
    _warn(f"unclosed event loop {self!r}", ResourceWarning, source=self)
  Enable tracemalloc to get traceback where the object was allocated.
  See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================================================================ 2 passed, 1 warning in 0.02s ===========================================================================

graingert avatar Dec 15 '23 12:12 graingert

full demo here https://github.com/kevin1024/vcrpy/commit/d0093369e43519a15ac785bf5961abbafc711be5

more minimal reproducer here https://github.com/graingert/asyncio_unclosed_loop

graingert avatar Dec 15 '23 12:12 graingert

I repeated this with e1415c1

graingert avatar Dec 15 '23 12:12 graingert

I have the same issue in versions 0.22.0+, including 0.23.0a0.

I first spent a ton of time trying to hunt down a new ResourceWarning I seemingly randomly started getting in my tests (maybe due to the more tests I added and thus more time to fail with the resource warning, unsure):

>               warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E               pytest.PytestUnraisableExceptionWarning: Exception ignored in: <socket.socket fd=-1, family=1, type=1, proto=0>
E               
E               Traceback (most recent call last):
E                 File "/Users/phillip/.pyenv/versions/3.11.6/lib/python3.11/json/decoder.py", line 353, in raw_decode
E                   obj, end = self.scan_once(s, idx)
E                              ^^^^^^^^^^^^^^^^^^^^^^
E               ResourceWarning: unclosed <socket.socket fd=13, family=1, type=1, proto=0>

I couldn't for the life of me figure out where I had an unclosed socket. So I finally admitted defeat and silenced the ResourceWarning, only to have this next error happen:

>               warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E               pytest.PytestUnraisableExceptionWarning: Exception ignored in: <function BaseEventLoop.__del__ at 0x104bed760>
E               
E               Traceback (most recent call last):
E                 File "/Users/phillip/.pyenv/versions/3.11.6/lib/python3.11/asyncio/base_events.py", line 692, in __del__
E                   _warn(f"unclosed event loop {self!r}", ResourceWarning, source=self)
E               ResourceWarning: unclosed event loop <_UnixSelectorEventLoop running=False closed=False debug=False>

The results seemed to change depending on how many tests I was running in my test suite.

Downgrading to 0.21.1 fixed both problems.

phillipuniverse avatar Dec 19 '23 06:12 phillipuniverse

Thanks for the report! I can reproduce the issue with pytest-asyncio v0.23.2. I can also reproduce it with pytest-asyncio v0.21.1, though:

$ python -X dev -m pytest --setup-show
===== test session starts =====
platform linux -- Python 3.12.1, pytest-7.4.3, pluggy-1.3.0
rootdir: /tmp/tst
plugins: asyncio-0.21.1
asyncio: mode=Mode.STRICT
collected 2 items                                                                                                                   

test_a.py 
        SETUP    F event_loop
        SETUP    F demo (fixtures used: event_loop)
        test_a.py::test_aiohttp_test_client_json (fixtures used: demo, event_loop, request).
        TEARDOWN F demo
        TEARDOWN F event_loop
        test_a.py::test_redirect.

===== warnings summary =====test_a.py::test_redirect
  /usr/lib/python3.12/asyncio/base_events.py:723: ResourceWarning: unclosed event loop <_UnixSelectorEventLoop running=False closed=False debug=True>
    _warn(f"unclosed event loop {self!r}", ResourceWarning, source=self)
  Enable tracemalloc to get traceback where the object was allocated.
  See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
===== 2 passed, 1 warning in 0.02s =====

This means it's probably not directly related to the v0.23 release.

Based on the console output above, my current understanding is that pytest(-asyncio) runs the async test normally. When the event_loop fixture is torn down, a fixture finalizer creates a new event loop to prevent subsequent non-async tests from having to deal with a closed loop:

https://github.com/pytest-dev/pytest-asyncio/blob/b614c77dec6df7414fba78d2fbe7989c6ee16828/pytest_asyncio/plugin.py#L824-L833

When the sync test calls asyncio.run, the loop created by the fixture finalizer is not closed by the asyncio Runner. I very recently encountered the same problem in a different issue.

@graingert I'm aware you're involved in upstream asyncio development. Do you think it's worth filing an enhancement issue for asyncio.run (or rather asyncio.Runner) to close any existing loop before setting a new loop?

@phillipuniverse If you don't use asyncio.run, it's possible that your issue has a separate cause. I can help more, if you provide a reproducer for you specific issue.

seifertm avatar Dec 24 '23 14:12 seifertm

@seifertm currently I'm trying to remove the policy system and get/set event loop in favour of loop_factory so this becomes a non-issue.

I think to fix this instead of using _provide_clean_event_loop you should call set_event_loop_policy(None) so asyncio.get_event_loop() returns to its original behaviour

graingert avatar Dec 24 '23 14:12 graingert

@seifertm I ran into this problem again and now realize that I do indeed have the same problem as @graingert with asyncio.run(). The MRE included at https://github.com/graingert/asyncio_unclosed_loop would replicate my issue as well. Although, same as you described the MRE also fails on 0.21.1...

Maybe 1 more data point - the issue for me only popped up in Python 3.12.2. I had a test suite that ran fine with Python 3.11.4 and when I went to 3.12.2 I got the failure.

I ended up hunting it down to executing Alembic migrations which was super non-obvious. [The recommended approach]9https://alembic.sqlalchemy.org/en/latest/cookbook.html#using-asyncio-with-alembic) for asyncio with alembic involves using asyncio.run(). The really confusing part is that even when I narrowed it down to this code path, it was still flaky. For instance I could add/remove a particular migration file and sometimes it would fail, sometimes it wouldn't. The migration files didn't do anything interesting just created some database columns.

It makes me think there is some sort of timing thing going on too, like maybe the event loop has to hang around for long enough such that it exhibits this problem, maybe some sort of race condition somewhere.

phillipuniverse avatar Apr 24 '24 17:04 phillipuniverse

I think this is the root cause of many of the remaining failing tests in aiohttp while migrating to pytest-asyncio. It seems that if we use loop.run_until_complete() in a standard test, then we get: ResourceWarning: unclosed <socket.socket fd=21, family=1, type=1, proto=0>

e.g. https://github.com/aio-libs/aiohttp/actions/runs/14848625605/job/41687854837?pr=10762 https://github.com/aio-libs/aiohttp/pull/10762

This is a blocking issue to migrate aiohttp over.

Dreamsorcerer avatar May 06 '25 00:05 Dreamsorcerer

The issue is reproducible in v1.0.0a1.

seifertm avatar May 23 '25 04:05 seifertm

I've been fixing some of the issues on aiohttp with v1. One case where we see this problem is when a unittest appears in our pytest test suite. e.g. The problem occurs with just an empty test case in our test suite:

class TestFoo(unittest.IsolatedAsyncioTestCase):
    async def test_domain_filter_same_host(self) -> None:
        pass

Dreamsorcerer avatar Jun 22 '25 15:06 Dreamsorcerer

As we have our own AiohttpTestCase subclass that needs testing, I don't see anyway we can workaround that: https://github.com/aio-libs/aiohttp/blob/e0d2f82f128dfa9124c280b343937f8fe00533a1/tests/test_test_utils.py#L92-L130

Dreamsorcerer avatar Jun 22 '25 22:06 Dreamsorcerer

I've been fixing some of the issues on aiohttp with v1. One case where we see this problem is when a unittest appears in our pytest test suite. e.g. The problem occurs with just an empty test case in our test suite:

class TestFoo(unittest.IsolatedAsyncioTestCase):
    async def test_domain_filter_same_host(self) -> None:
        pass

Per #433, it looks like pytest-asyncio doesn't support IsolatedAsyncioTestCase. The outcome is also unclosed event loop, but I think this issue is about seeing that warning in absence of unittest. (correct me if I'm wrong)

sbrudenell avatar Aug 05 '25 22:08 sbrudenell

Well, that's a rather old comment, but we'll need it working before aiohttp can migrate. It seems to be the same error (and likely same cause) as loop.run_until_complete(), so if that gets fixed it may then be working regardless.

Dreamsorcerer avatar Aug 06 '25 00:08 Dreamsorcerer

Hi there, i've got the same problem with an empty test, if I use async def I got one of my other test failing with unclosed event loop...

Do we have a workaround this ?

teva-krief avatar Sep 29 '25 15:09 teva-krief

Hi there, i've got the same problem with an empty test, if I use async def I got one of my other test failing with unclosed event loop...

Do we have a workaround this ?

I tend to specify the following in my pyproject.toml to circumvent this @teva-krief

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_test_loop_scope = "session"
asyncio_default_fixture_loop_scope = "session"

dada-engineer avatar Sep 29 '25 15:09 dada-engineer

I ended up using the same thing as you, thank you very much!

teva-krief avatar Sep 29 '25 16:09 teva-krief

For any other Googlers, this issue is particularly provoked by using the MongoDB motor driver in your tests. The workaround here works a treat.

shufflebits avatar Oct 16 '25 08:10 shufflebits