From fe350a8e41fc634dec497740a9ec9683be6e439f Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Wed, 26 Aug 2026 18:36:57 -0700 Subject: [PATCH] Reap the ping event when job_not_running exits via is_finished saltnado._disbatch_local spawns job_not_running as an IOLoop.spawn_callback coroutine to ping saltutil.find_job while the outer job is in flight. When the outer job's is_finished future completes first, job_not_running resolves the in-flight ping future via event.set_result(None) and returns. set_result() alone does not remove the future from event_listener.tag_map or timeout_map -- the cleanup path in _handle_event_socket_recv only fires when a matching event arrives on the socket, and clean_by_request only fires from the handler's on_finish. Because spawn_callback runs independently of the handler, the ping future may be registered *after* on_finish -> clean_by_request has already emptied request_map, so the leaked entry only clears when the gather_job_timeout callback fires (10s default, 30s under the netapi test fixture). The intermittent AssertionError in test_mem_leak_in_event_listener catches this leak whenever the 1s poll window closes before the timeout callback runs -- the race is triggered by ordinary event-loop scheduling variance, which explains why the test flakes even on 3008.x nightlies that then pass on retry. Fix: after set_result(None), explicitly cancel the timeout callback and call _timeout_future(...) to drop the entry from tag_map, matching the cleanup that clean_by_request would have performed had the request still been tracked. Fixes #70152 --- changelog/70152.fixed.md | 1 + salt/netapi/rest_tornado/saltnado.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 changelog/70152.fixed.md diff --git a/changelog/70152.fixed.md b/changelog/70152.fixed.md new file mode 100644 index 00000000000..8baa57ff839 --- /dev/null +++ b/changelog/70152.fixed.md @@ -0,0 +1 @@ +Plug the ``event_listener.tag_map`` / ``timeout_map`` leak in ``saltnado.job_not_running`` when the outer job finishes with an in-flight ``saltutil.find_job`` ping. diff --git a/salt/netapi/rest_tornado/saltnado.py b/salt/netapi/rest_tornado/saltnado.py index bca14013fbe..20954ca596c 100644 --- a/salt/netapi/rest_tornado/saltnado.py +++ b/salt/netapi/rest_tornado/saltnado.py @@ -1172,6 +1172,25 @@ def job_not_running(self, jid, tgt, tgt_type, minions, is_finished): if f is is_finished: if not event.done(): event.set_result(None) + # ``set_result(None)`` resolves the Future but leaves + # it in ``event_listener.tag_map`` / ``timeout_map``. + # ``job_not_running`` runs as a ``spawn_callback`` + # coroutine independent of the handler; if it has + # already registered a ping wait when the handler's + # ``on_finish`` -> ``clean_by_request`` runs, that + # entry escapes cleanup and lingers up to + # ``gather_job_timeout`` seconds until the timeout + # callback fires. Reap it inline so the maps drain + # promptly (see ``test_mem_leak_in_event_listener``). + listener = self.application.event_listener + timeout_handle = listener.timeout_map.pop(event, None) + if timeout_handle is not None: + tornado.ioloop.IOLoop.current().remove_timeout(timeout_handle) + listener._timeout_future( + ping_tag, + EventListener.prefix_matcher, + event, + ) raise tornado.gen.Return(True) event = f.result() except TimeoutException: