Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package salt for openSUSE:Factory checked in at 2026-09-04 12:37:03 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/salt (Old) and /work/SRC/openSUSE:Factory/.salt.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "salt" Fri Sep 4 12:37:03 2026 rev:204 rq:1375636 version:3006.0 Changes: -------- --- /work/SRC/openSUSE:Factory/salt/salt.changes 2026-08-28 19:46:41.563530180 +0200 +++ /work/SRC/openSUSE:Factory/.salt.new.1265/salt.changes 2026-09-04 12:37:56.176370862 +0200 @@ -1,0 +2,16 @@ +Thu Sep 3 14:19:52 UTC 2026 - Marek Czernek <[email protected]> + +- Fix test_tcp for pytest >=8 + +- Added: + * fix-test_tcp-for-pytest-8-779.patch + +------------------------------------------------------------------- +Tue Sep 1 08:09:05 UTC 2026 - Pablo Suárez Hernández <[email protected]> + +- Fix file handlers leaking on using SyncWrapper (bsc#1272939) + +- Added: + * fix-file-handlers-leaking-on-using-syncwrapper-bsc-1.patch + +------------------------------------------------------------------- New: ---- fix-file-handlers-leaking-on-using-syncwrapper-bsc-1.patch fix-test_tcp-for-pytest-8-779.patch ----------(New B)---------- New:- Added: * fix-file-handlers-leaking-on-using-syncwrapper-bsc-1.patch New:- Added: * fix-test_tcp-for-pytest-8-779.patch ----------(New E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ salt.spec ++++++ --- /var/tmp/diff_new_pack.PzTp4L/_old 2026-09-04 12:38:00.499522729 +0200 +++ /var/tmp/diff_new_pack.PzTp4L/_new 2026-09-04 12:38:00.503522870 +0200 @@ -682,6 +682,13 @@ # PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/778 # PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/68946 Patch220: honor-proxy-settings-in-gitfs-git_pillar-and-winrepo.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/777 +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/68456 +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/commit/64aae7bff10cbfa48c352a4a40b31b941e47f689 +Patch221: fix-file-handlers-leaking-on-using-syncwrapper-bsc-1.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/779 +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/70172 +Patch222: fix-test_tcp-for-pytest-8-779.patch ### IMPORTANT: The line below is used as a snippet marker. Do not touch it. ### SALT PATCHES LIST END ++++++ _lastrevision ++++++ --- /var/tmp/diff_new_pack.PzTp4L/_old 2026-09-04 12:38:00.625527156 +0200 +++ /var/tmp/diff_new_pack.PzTp4L/_new 2026-09-04 12:38:00.629527296 +0200 @@ -1,2 +1,3 @@ -97b7927a6d91df9504b0f884780a3605664202d0 +60b35054672d42a7cd634d7aef745bf2df2e6e17 +(No newline at EOF) ++++++ fix-file-handlers-leaking-on-using-syncwrapper-bsc-1.patch ++++++ >From 016cbde7805a4aadee1c3bbe72fb453e750deea7 Mon Sep 17 00:00:00 2001 From: Victor Zhestkov <[email protected]> Date: Tue, 1 Sep 2026 10:01:31 +0200 Subject: [PATCH] Fix file handlers leaking on using SyncWrapper (bsc#1272939) (#777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: descriptor leaking in salt.utils.http.query * chore: add tests * Guard SyncWrapper teardown against leaking shutdown_* coroutines Root cause of "RuntimeWarning: coroutine 'BaseEventLoop.shutdown_asyncgens' was never awaited" on Python 3.14 / Windows in the batch CLI tests: loop.run_until_complete(loop.shutdown_asyncgens()) evaluates the inner argument FIRST -- creating the ``shutdown_asyncgens`` coroutine object -- and only THEN runs ``run_until_complete`` which calls ``_check_closed()`` / ``_check_running()``. If either check raises (the loop has already been closed or is already running for some other reason), the ``RuntimeError`` propagates before ``ensure_future`` can wrap the coroutine into a Task, the bare coroutine object is orphaned, and ``coroutine.__del__`` emits the warning when the GC reaps it. On Python 3.14 / Windows that happens to land while the outer loop's ``_ready.clear()`` is mid-flight, which is what put ``self._ready.clear()`` in the warning's traceback in the failing ``test_batch_retcode`` / ``test_multiple_modules_in_batch`` jobs. Same trap for ``shutdown_default_executor`` and for the ``asyncio.gather(...)`` over cancelled pending tasks. Fix: * New ``SyncWrapper._loop_can_run_until_complete(loop)`` helper — ``True`` iff ``loop is not None and not loop.is_closed() and not loop.is_running()``. Anything that can't be driven to completion is gated out before the coroutine is ever constructed. * In ``SyncWrapper.close``, gate every ``run_until_complete`` call through that helper. As a belt-and-braces, on the ``except`` arm explicitly ``.close()`` the (now-orphaned) coroutine so even a loop-state race between the guard and the call can't leak. Replaces the previous ``asyncio.sleep(0)`` drain workaround introduced in commit d3bfd533e50, which only papered over the leak by giving the GC more wallclock to run the scheduled-but-unawaited coroutines before ``close()`` cleared them. Acceptance bar (Linux Py3.10): python -W error::RuntimeWarning -X dev -m pytest \ tests/pytests/integration/cli/test_batch.py::test_batch_retcode \ --core-tests -xvs PASSES. Full sweep (9 integration + 74 unit batch tests + ``test_fd_leak_asyncgens_executor.py`` + ``test_fd_leak_task_cancellation.py``) under the same flags: 85 passed, 0 failed. --------- Co-authored-by: Vitaliy Vasylenko <[email protected]> Co-authored-by: Daniel A. Wozniak <[email protected]> --- salt/utils/asynchronous.py | 84 +++++++++++++++++++- salt/utils/http.py | 11 ++- tests/pytests/unit/utils/test_http.py | 109 ++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 6 deletions(-) diff --git a/salt/utils/asynchronous.py b/salt/utils/asynchronous.py index 403b2dbdc30..5e22a12b5ae 100644 --- a/salt/utils/asynchronous.py +++ b/salt/utils/asynchronous.py @@ -124,6 +124,28 @@ class SyncWrapper: def __repr__(self): return f"<SyncWrapper(cls={self.cls})" + @staticmethod + def _loop_can_run_until_complete(loop): + """ + Return ``True`` iff ``loop.run_until_complete(coro)`` can drive a + freshly created coroutine to completion without immediately raising. + + ``BaseEventLoop.run_until_complete`` raises ``RuntimeError`` if the + loop is closed or already running, but only *after* its + ``future`` argument has been evaluated. Constructing the + coroutine without being able to await it leaks it through + ``coroutine.__del__`` as ``RuntimeWarning: coroutine '...' was + never awaited`` (see ``close()``). We avoid that by inspecting + the loop's state up front. + """ + if loop is None: + return False + if loop.is_closed(): + return False + if loop.is_running(): + return False + return True + def close(self): for method in self._close_methods: if method in self._async_methods: @@ -138,8 +160,66 @@ class SyncWrapper: method() except AttributeError: log.error("No async method %s on object %r", method, self.obj) - except Exception: # pylint: disable=broad-except - log.exception("Exception encountered while running stop method") + except Exception as exc: # pylint: disable=broad-except + log.exception( + "Exception encountered while running stop method: %s", exc + ) + # Shut down asyncio resources before closing the IOLoop so file descriptors + # held by pending tasks, async generators, and the default executor are released. + # + # Each of the three ``run_until_complete`` calls below takes a freshly + # constructed coroutine object as its argument. If the loop is already + # closed (or running) at that point ``run_until_complete`` raises + # ``RuntimeError`` *after* the coroutine has been created but *before* + # ``ensure_future`` wraps it — and the bare coroutine object is then + # garbage-collected unawaited, emitting a + # ``RuntimeWarning: coroutine '...' was never awaited`` on stderr. On + # Python 3.14 / Windows the batch CLI integration tests + # (``tests/pytests/integration/cli/test_batch.py::test_batch_retcode`` + # and ``test_multiple_modules_in_batch``) gate on ``assert not + # cmd.stderr`` and turn that warning into a hard failure. + # + # Gate every call on ``not _loop_can_run_until_complete(loop)`` so we + # never even *construct* the inner coroutine when the loop can't drive + # it to completion. + try: + if self._loop_can_run_until_complete(self.asyncio_loop): + pending_tasks = [ + task + for task in asyncio.all_tasks(self.asyncio_loop) + if not task.done() + ] + if pending_tasks: + for task in pending_tasks: + task.cancel() + gathered = asyncio.gather(*pending_tasks, return_exceptions=True) + try: + self.asyncio_loop.run_until_complete(gathered) + except Exception: # pylint: disable=broad-except + # ``gathered`` is a Future; if run_until_complete bailed + # part-way we still need to make sure the Future is + # consumed so its exception (if any) isn't logged as + # unhandled. Tasks already cancelled above. + if not gathered.done(): + gathered.cancel() + + if self._loop_can_run_until_complete(self.asyncio_loop): + shutdown_agens = self.asyncio_loop.shutdown_asyncgens() + try: + self.asyncio_loop.run_until_complete(shutdown_agens) + except Exception: # pylint: disable=broad-except + shutdown_agens.close() + + if self._loop_can_run_until_complete(self.asyncio_loop): + shutdown_exec = self.asyncio_loop.shutdown_default_executor() + try: + self.asyncio_loop.run_until_complete(shutdown_exec) + except Exception: # pylint: disable=broad-except + shutdown_exec.close() + + except Exception as exc: # pylint: disable=broad-except + log.error("Error during asyncio shutdown: %s", exc) + io_loop = self.io_loop io_loop.stop() try: diff --git a/salt/utils/http.py b/salt/utils/http.py index bc9e095d32d..7b2d8a518f6 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -607,12 +607,15 @@ def query( req_kwargs = salt.utils.data.decode(req_kwargs, to_str=True) try: - download_client = SyncWrapper( + download_client_kwargs = ( + {"max_body_size": max_body} if supports_max_body_size else {} + ) + with SyncWrapper( AsyncHTTPClient, - kwargs={"max_body_size": max_body} if supports_max_body_size else {}, + kwargs=download_client_kwargs, async_methods=["fetch"], - ) - result = download_client.fetch(url_full, **req_kwargs) + ) as download_client: + result = download_client.fetch(url_full, **req_kwargs) except tornado.httpclient.HTTPError as exc: ret["status"] = exc.code ret["error"] = str(exc) diff --git a/tests/pytests/unit/utils/test_http.py b/tests/pytests/unit/utils/test_http.py index 52bf3d2ca28..cb397f4b196 100644 --- a/tests/pytests/unit/utils/test_http.py +++ b/tests/pytests/unit/utils/test_http.py @@ -1,10 +1,67 @@ import pytest import requests +import tornado.httpclient import salt.utils.http from tests.support.mock import MagicMock, patch +class _FakeFetchResponse: + """Simple object mimicking the bits of tornado's HTTPResponse we rely on.""" + + def __init__(self, code=200, body=b"payload", headers=None): + self.code = code + self.body = body + self.headers = headers or {"Content-Type": "text/plain; charset=utf-8"} + + [email protected] +def syncwrapper_stub(monkeypatch): + """Patch ``salt.utils.http.SyncWrapper`` with a controllable test double.""" + + class SyncWrapperStub: + fetch_return = _FakeFetchResponse() + fetch_side_effect = None + enter_calls = 0 + close_calls = 0 + fetch_calls = 0 + + def __init__(self, *args, **kwargs): + # Mirror SyncWrapper signature but we only need to track usage. + pass + + def __enter__(self): + SyncWrapperStub.enter_calls += 1 + return self + + def __exit__(self, exc_type, exc, tb): + self.close() + # Propagate exceptions so http.query can handle them + return False + + def close(self): + SyncWrapperStub.close_calls += 1 + + def fetch(self, *args, **kwargs): + SyncWrapperStub.fetch_calls += 1 + if SyncWrapperStub.fetch_side_effect is not None: + raise SyncWrapperStub.fetch_side_effect + return SyncWrapperStub.fetch_return + + @classmethod + def reset_counters(cls, *, clear_fetch=True): + cls.enter_calls = 0 + cls.close_calls = 0 + cls.fetch_calls = 0 + if clear_fetch: + cls.fetch_side_effect = None + cls.fetch_return = _FakeFetchResponse() + + monkeypatch.setattr(salt.utils.http, "SyncWrapper", SyncWrapperStub) + SyncWrapperStub.reset_counters() + return SyncWrapperStub + + def test_requests_session_verify_ssl_false(ssl_webserver, integration_files_dir): """ test salt.utils.http.session when using verify_ssl @@ -51,3 +108,55 @@ def test_session_ca_bundle(): with patch_os: ret = salt.utils.http.session(ca_bundle=fpath) assert ret.verify == fpath + + +def test_query_tornado_httperror_no_response(): + """ + Tests that http.query handles a Tornado HTTPError where exc.response is None. + This happens on connection-level failures such as a connect timeout (HTTP 599) + where no HTTP response is ever received from the server. + """ + import tornado.httpclient + + http_error = tornado.httpclient.HTTPError(599, "Timeout while connecting") + assert http_error.response is None + + mock_client = MagicMock() + mock_client.fetch.side_effect = http_error + # http.query() uses SyncWrapper as a context manager; ensure + # __enter__() returns the mock_client itself. + mock_client.__enter__.return_value = mock_client + + +def test_query_tornado_closes_syncwrapper_on_success(syncwrapper_stub): + syncwrapper_stub.reset_counters() + syncwrapper_stub.fetch_return = _FakeFetchResponse(body=b"test-body") + + ret = salt.utils.http.query("http://example.com", backend="tornado", status=True) + + assert syncwrapper_stub.enter_calls == 1 + assert syncwrapper_stub.close_calls == 1 + assert syncwrapper_stub.fetch_calls == 1 + assert ret["body"] == "test-body" + assert ret["status"] == 200 + + +def test_query_tornado_closes_syncwrapper_on_http_error(syncwrapper_stub): + syncwrapper_stub.reset_counters() + response = MagicMock(body=b"", headers={"Content-Type": "text/plain"}) + syncwrapper_stub.fetch_side_effect = tornado.httpclient.HTTPError( + 599, "Unit test failure", response=response + ) + + ret = salt.utils.http.query( + "http://example.com", + backend="tornado", + status=True, + raise_error=True, + ) + + assert syncwrapper_stub.enter_calls == 1 + assert syncwrapper_stub.close_calls == 1 + assert syncwrapper_stub.fetch_calls == 1 + assert ret["status"] == 599 + assert "Unit test failure" in ret["error"] -- 2.55.0 ++++++ fix-test_tcp-for-pytest-8-779.patch ++++++ >From 4d431545a6722c7312b1c09ebbc855d25ebb0ed2 Mon Sep 17 00:00:00 2001 From: Marek Czernek <[email protected]> Date: Thu, 3 Sep 2026 16:12:24 +0200 Subject: [PATCH] Fix test_tcp for pytest >=8 (#779) --- tests/unit/transport/test_tcp.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/transport/test_tcp.py b/tests/unit/transport/test_tcp.py index 934a040093..d640a1570d 100644 --- a/tests/unit/transport/test_tcp.py +++ b/tests/unit/transport/test_tcp.py @@ -17,7 +17,7 @@ import tornado.gen import tornado.ioloop import salt.utils.platform import salt.utils.process -from tornado.testing import AsyncTestCase +import tornado.testing from tests.support.mixins import AdaptedConfigurationTestCaseMixin from tests.unit.transport.mixins import run_loop_in_thread @@ -30,11 +30,14 @@ log = logging.getLogger(__name__) @pytest.mark.skip(reason="Skip until we can devote time to fix this test") -class AsyncPubServerTest(AsyncTestCase, AdaptedConfigurationTestCaseMixin): +class AsyncPubServerTest(tornado.testing.AsyncTestCase, AdaptedConfigurationTestCaseMixin): """ Tests around the publish system """ + def runTest(self): + pass + @classmethod def setUpClass(cls): ret_port = ports.get_unused_localhost_port() -- 2.55.0
