Skip to content

Fix master-initiated jobs on Py3.12 by installing asyncio loop in SyncWrapper (#65702)#69482

Open
dwoz wants to merge 2 commits into
saltstack:3006.xfrom
dwoz:fix/issue-65702
Open

Fix master-initiated jobs on Py3.12 by installing asyncio loop in SyncWrapper (#65702)#69482
dwoz wants to merge 2 commits into
saltstack:3006.xfrom
dwoz:fix/issue-65702

Conversation

@dwoz

@dwoz dwoz commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes salt-CLI publishes failing on Python 3.12+ with RuntimeError: There is no current event loop in thread 'Thread-N (_target)'.

salt.utils.asynchronous.SyncWrapper spawns a worker thread
(threading.Thread(target=self._target, ...) -> Python names it
"Thread-N (_target)") and runs io_loop.run_sync(...) inside it.
Any wrapped coroutine that touches asyncio.get_event_loop() -
notably pyzmq's zmq.eventloop.future sockets used by every
LocalClient.pub send through AsyncReqChannel - hits that call
from the worker thread, and on Python 3.12+ it raises because the
implicit per-thread loop creation was removed.

Create a dedicated asyncio.new_event_loop() per SyncWrapper and
install it on the worker thread via asyncio.set_event_loop()
before io_loop.run_sync(...). Close the asyncio loop in
SyncWrapper.close. The vendored Tornado IOLoop on 3006.x continues
to drive the coroutine itself; this change only ensures
asyncio.get_event_loop() returns a usable loop on the worker
thread for any code path that asks for one.

What issues does this PR fix or reference?

Fixes #65702

Previous Behavior

On Python 3.12+ (Fedora 39+, Ubuntu 24.04, etc.), running
salt '*' test.ping or salt '*' state.apply on the salt master
produced:

[TRACE   ] Failed to send msg RuntimeError("There is no current event loop in thread 'Thread-2 (_target)'.")

and the publish was dropped, so no minion ever ran the job. Every
master-initiated job collapsed.

New Behavior

SyncWrapper installs an asyncio event loop on its worker thread,
so pyzmq's zmq.eventloop.future socket calls (and any other code
that calls asyncio.get_event_loop()) succeed and the publish
goes through normally.

Regression test

tests/pytests/unit/utils/test_asynchronous.py:: test_sync_wrapper_thread_has_asyncio_loop_65702 constructs a
SyncWrapper around a coroutine that calls
asyncio.get_event_loop() from inside io_loop.run_sync. Without
this patch it fails on Py3.12+ with the exact reported error.
With the patch it passes on both Py3.10 and Py3.12.

Merge requirements satisfied?

  • Docs (no user-facing doc change; internal threading fix)
  • Changelog (changelog/65702.fixed.md)
  • Tests written/updated

Commits signed with GPG?

Yes

…cWrapper

On Python 3.12+ the SyncWrapper worker thread had no asyncio event
loop installed, so any wrapped coroutine that touched
``asyncio.get_event_loop()`` raised ``RuntimeError: There is no current
event loop in thread 'Thread-N (_target)'`` and aborted the call.

This breaks every master-initiated job (``salt '*' test.ping``,
``state.apply``, etc.) on the salt master CLI because ``LocalClient.pub``
sends through ``AsyncReqChannel`` wrapped by ``SyncWrapper``, and the
underlying pyzmq ``zmq.eventloop.future`` socket calls
``asyncio.get_event_loop()`` from the worker thread.

Python 3.12 removed the implicit auto-creation of an event loop in
non-main threads, so ``get_event_loop`` raises instead of silently
constructing one.

Create a dedicated ``asyncio.new_event_loop()`` per SyncWrapper and
install it on the worker thread via ``asyncio.set_event_loop()`` before
``io_loop.run_sync(...)``.  Close the asyncio loop in ``SyncWrapper.close``.

Fixes saltstack#65702
The top-level 'import asyncio' added in the previous commit broke salt-ssh
tests against the centos py36 target container: that thin tarball does not
ship 'typing_extensions', so the asyncio -> contextvars -> immutables
-> typing_extensions import chain fails at module load.  Move the import
inside SyncWrapper.__init__ and SyncWrapper._target where it is actually
used, leaving the rest of salt.utils.asynchronous importable on legacy
py3.6/3.7 ssh thin targets.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:full Run the full test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant