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

0.25.1 regression Python 3.9 asyncio_mode="auto" # RuntimeError: There is no current event loop in thread 'MainThread' #1039

Closed
dimaqq opened this issue Jan 7, 2025 · 6 comments
Milestone

Comments

@dimaqq
Copy link

dimaqq commented Jan 7, 2025

Happy new year.

0.25.1 was released recently and somehow it fails on Python 3.9 specifically.

In my case, asyncio_mode = "auto" is used, so maybe similar to #658, however I don't know if that's required for the regression.

Meanwhile, Python 3.8 is OK (0.25.x can't be used, 0.24 is used instead)
And Python 3.10, 3.11, 3.12, 3.13 are OK (with the new version)

Version resolution:

    { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
    { name = "pytest-asyncio", version = "0.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },

Run log: https://github.com/juju/python-libjuju/actions/runs/12423507633

Screenshot for the above:
Screenshot 2025-01-07 at 14 43 51

I've manually validated that 0.25.0 works fine in my setup.

@dimaqq
Copy link
Author

dimaqq commented Jan 7, 2025

Log, if helps any:

self = <tests.unit.test_model.TestModelState testMethod=test_apply_delta>

    def test_apply_delta(self):
>       model = Model()

tests/unit/test_model.py:72:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
juju/model/__init__.py:677: in __init__
    self._watch_stopping = asyncio.Event()
/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/asyncio/locks.py:177: in __init__
    self._loop = events.get_event_loop()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <asyncio.unix_events._UnixDefaultEventLoopPolicy object at 0x105a521f0>

    def get_event_loop(self):
        """Get the event loop for the current context.

        Returns an instance of EventLoop or raises an exception.
        """
        if (self._local._loop is None and
                not self._local._set_called and
                threading.current_thread() is threading.main_thread()):
            self.set_event_loop(self.new_event_loop())

        if self._local._loop is None:
>           raise RuntimeError('There is no current event loop in thread %r.'
                               % threading.current_thread().name)
E           RuntimeError: There is no current event loop in thread 'MainThread'.

@dimaqq
Copy link
Author

dimaqq commented Jan 7, 2025

My config, since the most likely cause is the change that fixed interleaved loop scopes:

[tool.pytest.ini_options]
addopts = "-ra -v"
asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
filterwarnings = "ignore::DeprecationWarning:websockets"
markers = [
    "serial: mark a test that must run by itself",
    "wait_for_idle: mark a test that waits for the model to be idle",
    "bundle: mark a test that uses a bundle",
]

I don't have custom loop_scope's in the tests, only this config.

I do, however, have a mix of modern pytest tests (module-level test_foo(): ...) and older class-based tests class TestFoo(unittest.TestCase): def test_foo(self): ...)

Easy reproducer:

git clone [email protected]:juju/python-libjuju.git
cd python-libjuju
# currently at a58645e
uvx -p 3.9 tox -e unit

@seifertm
Copy link
Contributor

seifertm commented Jan 8, 2025

Thanks for the detailed report!
The failing test is a synchronous test. Maybe #1029 introduced a regression here.

jujubot added a commit to juju/python-libjuju that referenced this issue Jan 9, 2025
#1250

- update type hints in juju/application.py, pyright is now happy
- correct type hints for facade.Type
- work around pytest-dev/pytest-asyncio#1039
@dimaqq
Copy link
Author

dimaqq commented Jan 9, 2025

Yes the test is synchronous, however it instantiates some code which in turn creates an asyncio.Event().

suutari-ai added a commit to andersinno/passari-workflow that referenced this issue Jan 10, 2025
Replace the asyncio.get_event_loop() followed by run_until_complete(...)
call with the asyncio.run(...), because it makes the code cleaner and
works even if the event loop in the main thread has been cleared.

This fixes an issue with pytest-asyncio 0.25.2 which seems to clear the
main thread event loop sometimes.  The tests on sync_attachments and
sync_objects used to fail if all tests were ran in the same pytest call,
but if only those sync tests are selected (e.g. with `pytest -k
test_sync`) then the tests used to pass.  After this change they seem to
pass on both cases.

This might be the same issue as described in [1] (even if it is only for
Python 3.9, but our issue happens with Python 3.12) or not.

[1] pytest-dev/pytest-asyncio#1039
@seifertm seifertm added this to the v0.25 milestone Jan 15, 2025
@bflad
Copy link

bflad commented Feb 4, 2025

Also have received a similar traceback for this test definition:

@pytest.mark.asyncio()
async def test_global_retry_config_disable_async():
    s = SDK(retry_config=None)

    with pytest.raises(
        errors.APIError, match="API error occurred: Status 503"
    ) as exc_info:
        await s.retries.retries_get_async(request_id=str(uuid.uuid4()), num_retries=2)

    assert exc_info.value.status_code == 503
  [gw0] linux -- Python 3.9.21 /home/runner/work/xxx/.venv/bin/python
  
  self = <Coroutine test_global_retry_config_disable_async>
  
      def runtest(self) -> None:
          self.obj = wrap_in_sync(
              # https://github.com/pytest-dev/pytest-asyncio/issues/596
              self.obj,  # type: ignore[has-type]
          )
  >       super().runtest()
  
  .venv/lib/python3.9/site-packages/pytest_asyncio/plugin.py:533: 
  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
  .venv/lib/python3.9/site-packages/pytest_asyncio/plugin.py:1049: in inner
      _loop = _get_event_loop_no_warn()
  .venv/lib/python3.9/site-packages/pytest_asyncio/plugin.py:979: in _get_event_loop_no_warn
      return asyncio.get_event_loop()
  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
  
  self = <asyncio.unix_events._UnixDefaultEventLoopPolicy object at 0x7fd55aef8ee0>
  
      def get_event_loop(self):
          """Get the event loop for the current context.
      
          Returns an instance of EventLoop or raises an exception.
          """
          if (self._local._loop is None and
                  not self._local._set_called and
                  threading.current_thread() is threading.main_thread()):
              self.set_event_loop(self.new_event_loop())
      
          if self._local._loop is None:
  >           raise RuntimeError('There is no current event loop in thread %r.'
                                 % threading.current_thread().name)
  E           RuntimeError: There is no current event loop in thread 'MainThread'.
  
  /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/asyncio/events.py:642: RuntimeError

@seifertm
Copy link
Contributor

seifertm commented Feb 8, 2025

I found some time to dig into this. Let me outline my findings.

As @dimaqq already mentioned, the failing test calls asyncio.Event() and only fails on Python 3.9. The implementation of the Event constructor in CPython 3.9 calls asyncio.get_event_loop() which is also seen in the stacktrace. In CPython 3.9 this function behaves differently depending on whether asyncio.set_event_loop() has been called or not: If set_event_loop has never been called before, get_event_loop will return a fresh loop. However, if set_event_loop has already been called on the current event loop policy, get_event_loop will raise the reported RuntimeError (see the docs). Later implementations of asyncio.Event() from CPython 3.10 onwards no longer call asyncio.get_event_loop(), which is why they don't produce the same error.

Indeed, running the failing test "test_apply_delta" on its own causes the test to run successfully, whereas running the full test suite makes the test fail when pytest-asyncio v0.25.1 is installed.

Therefore, I incrementally disabled tests from the test suite and found that pytest -k "test_controller or test_apply_delta" causes test_apply_delta to fail. When I inspected the test fixtures with pytest's --setup-show option, I saw that none of the tests use pytest-asyncio fixtures. The tests in the test_controller module use unittest.IsolatedAsyncioTestCase, which happens to call asyncio.set_event_loop() during the test run. This explains why the subsequent call to asyncio.Event() fails during test_apply_delta.

Most importantly, the tests produce the same error even when pytest-asyncio is disabled with -p no:asyncio:

$ tox -e unit
unit: commands[0]> pytest -x --setup-show -k 'test_controller or test_apply_delta' -p no:asyncio /tmp/python-libjuju/tests/unit
===== test session starts =====
platform linux -- Python 3.9.21, pytest-8.3.4, pluggy-1.5.0 -- /tmp/python-libjuju/.tox/py3/bin/python
cachedir: .tox/py3/.pytest_cache
rootdir: /tmp/python-libjuju
configfile: pyproject.toml
collected 260 items / 250 deselected / 10 selected                                                                                                                                                                    

tests/unit/test_controller.py::TestControllerConnect::test_file_cred_v2 
      SETUP    C _unittest_setUpClass_fixture_TestControllerConnect
        tests/unit/test_controller.py::TestControllerConnect::test_file_cred_v2 (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_file_cred_v3 
        tests/unit/test_controller.py::TestControllerConnect::test_file_cred_v3 (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_no_args 
        tests/unit/test_controller.py::TestControllerConnect::test_no_args (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_with_controller_name 
        tests/unit/test_controller.py::TestControllerConnect::test_with_controller_name (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_bakery_client 
        tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_bakery_client (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_macaroons 
        tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_macaroons (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_no_auth 
        tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_no_auth (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_userpass 
        tests/unit/test_controller.py::TestControllerConnect::test_with_endpoint_and_userpass (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
tests/unit/test_controller.py::TestControllerConnect::test_with_posargs 
        tests/unit/test_controller.py::TestControllerConnect::test_with_posargs (fixtures used: _unittest_setUpClass_fixture_TestControllerConnect, request)PASSED
      TEARDOWN C _unittest_setUpClass_fixture_TestControllerConnect
tests/unit/test_model.py::TestModelState::test_apply_delta 
      SETUP    C _unittest_setUpClass_fixture_TestModelState
        tests/unit/test_model.py::TestModelState::test_apply_delta (fixtures used: _unittest_setUpClass_fixture_TestModelState, request)FAILED
      TEARDOWN C _unittest_setUpClass_fixture_TestModelState

===== FAILURES =====
_____ TestModelState.test_apply_delta _____

self = <tests.unit.test_model.TestModelState testMethod=test_apply_delta>

    def test_apply_delta(self):
>       model = Model()

tests/unit/test_model.py:72: 
_ _ _
juju/model/__init__.py:677: in __init__
    self._watch_stopping = asyncio.Event()
/usr/lib/python3.9/asyncio/locks.py:177: in __init__
    self._loop = events.get_event_loop()
_ _ _

self = <asyncio.unix_events._UnixDefaultEventLoopPolicy object at 0x7f00fa9d7fa0>

    def get_event_loop(self):
        """Get the event loop for the current context.
    
        Returns an instance of EventLoop or raises an exception.
        """
        if (self._local._loop is None and
                not self._local._set_called and
                threading.current_thread() is threading.main_thread()):
            self.set_event_loop(self.new_event_loop())
    
        if self._local._loop is None:
>           raise RuntimeError('There is no current event loop in thread %r.'
                               % threading.current_thread().name)
E           RuntimeError: There is no current event loop in thread 'MainThread'.

/usr/lib/python3.9/asyncio/events.py:642: RuntimeError
===== warnings summary =====
.tox/py3/lib/python3.9/site-packages/_pytest/config/__init__.py:1441
  /tmp/python-libjuju/.tox/py3/lib/python3.9/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope
  
    self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

.tox/py3/lib/python3.9/site-packages/_pytest/config/__init__.py:1441
  /tmp/python-libjuju/.tox/py3/lib/python3.9/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_mode
  
    self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

juju/client/connection.py:31
  /tmp/python-libjuju/juju/client/connection.py:31: DeprecationWarning: websockets.WebSocketClientProtocol is deprecated
    _WebSocket: TypeAlias = websockets.WebSocketClientProtocol

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
===== short test summary info =====
FAILED tests/unit/test_model.py::TestModelState::test_apply_delta - RuntimeError: There is no current event loop in thread 'MainThread'.
!!!!! stopping after 1 failures !!!!!
===== 1 failed, 9 passed, 250 deselected, 3 warnings in 0.66s =====
unit: exit 1 (0.90 seconds) /tmp/python-libjuju> pytest -x --setup-show -k 'test_controller or test_apply_delta' -p no:asyncio /tmp/python-libjuju/tests/unit pid=25775
  unit: FAIL code 1 (0.94=setup[0.04]+cmd[0.90] seconds)
  evaluation failed :( (1.02 seconds)
(

Therefore, I conclude that this is not a pytest-asyncio bug. The issue in the test suite was simply overshadowed by bad behavior of pytest-asyncio in earlier versions.

Changing line 70 of tests/unit/test_model.py from class TestModelState(unittest.TestCase): to class TestModelState(unittest.IsolatedAsyncioTestCase): fixes the issue on Python 3.9.

@bflad Your issue is likely a different one. The best way to solve it is to provide a minimal reproducible example in a separate issue.

@seifertm seifertm closed this as not planned Won't fix, can't repro, duplicate, stale Feb 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants