Skip to content

Restore __del__ safety-net cleanup on SyncWrapper + PublishServer + _… - #70261

Open
dwoz wants to merge 2 commits into
saltstack:3008.xfrom
dwoz:dwoz/fix/70175-del-cleanup-safety-net
Open

Restore __del__ safety-net cleanup on SyncWrapper + PublishServer + _…#70261
dwoz wants to merge 2 commits into
saltstack:3008.xfrom
dwoz:dwoz/fix/70175-del-cleanup-safety-net

Conversation

@dwoz

@dwoz dwoz commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

…TCPPubServerPublisher (#70175)

Extends the "warn + fall back to close()" pattern from commit 9955b89 (salt.utils.event.SaltEvent.__del__) to the three sub-classes SaltEvent composes with:

  • salt.utils.asynchronous.SyncWrapper.__del__
  • salt.transport.tcp.PublishServer.__del__
  • salt.transport.tcp._TCPPubServerPublisher.__del__

Each __del__ still emits the ResourceWarning via salt.utils.resource_warnings.warn_until_close (so leaky callers keep surfacing for pre-Potassium tracking), then falls back to close() wrapped in try/except so a finalizer never propagates. For PublishServer.close the individual sub-resource close steps (pub_sock, pub_server, pull_sock, io_loop.stop, io_loop.close) are additionally guarded, because they can raise during GC-time execution when the io_loop is in a partially torn-down state -- which is exactly the failure mode driving ~50 MB/hr RSS growth on the minion under sustained event traffic.

Companion to sibling branches dwoz/fix/70175-pubserver-perjob-leak, dwoz/fix/70175-saltevent-caller-close,
dwoz/fix/70175-receive-path-saltevent and to shutdown-path fix #70206. The master (Potassium) branch drops the close() fallback and requires explicit close() / context-manager use; the loud ResourceWarning here is the migration signal for that change.

Regression tests:

  • tests/pytests/unit/utils/test_asynchronous.py::test_syncwrapper_del_safety_net_calls_close_70175
  • tests/pytests/unit/transport/test_tcp.py::test_publish_server_del_safety_net_calls_close_70175
  • tests/pytests/unit/transport/test_tcp.py::test_tcppubserverpublisher_del_safety_net_calls_close_70175

Each test:

  1. Instantiates the class without with, drops the reference, forces GC
  2. Asserts the ResourceWarning still fires (behavior preserved)
  3. Asserts a class-appropriate observable that only close() would set (asyncio_loop.is_closed() for SyncWrapper; sub-resource close() mock calls for PublishServer; stream+socket close for _TCPPubServerPublisher)

All three tests fail on pre-patch origin/3008.x with only the ResourceWarning firing, and pass with the safety-net restored.

Refs #70175, #70206.

@dwoz
dwoz requested a review from a team as a code owner September 10, 2026 08:14
@dwoz dwoz added the test:full Run the full test suite label Sep 10, 2026
@dwoz
dwoz force-pushed the dwoz/fix/70175-del-cleanup-safety-net branch from d8c41b9 to f5e3aad Compare September 10, 2026 08:22
@dwoz
dwoz force-pushed the dwoz/fix/70175-del-cleanup-safety-net branch from f5e3aad to d26bbfe Compare September 10, 2026 09:09
…TCPPubServerPublisher (saltstack#70175)

Extends the "warn + fall back to close()" pattern from commit 9955b89
(``salt.utils.event.SaltEvent.__del__``) to the three sub-classes ``SaltEvent``
composes with:

* ``salt.utils.asynchronous.SyncWrapper.__del__``
* ``salt.transport.tcp.PublishServer.__del__``
* ``salt.transport.tcp._TCPPubServerPublisher.__del__``

Each ``__del__`` still emits the ``ResourceWarning`` via
``salt.utils.resource_warnings.warn_until_close`` (so leaky callers keep
surfacing for pre-Potassium tracking), then falls back to ``close()``
wrapped in try/except so a finalizer never propagates.  For
``PublishServer.close`` the individual sub-resource close steps
(``pub_sock``, ``pub_server``, ``pull_sock``, ``io_loop.stop``,
``io_loop.close``) are additionally guarded, because they can raise
during GC-time execution when the io_loop is in a partially torn-down
state -- which is exactly the failure mode driving ~50 MB/hr RSS
growth on the minion under sustained event traffic.

Companion to sibling branches ``dwoz/fix/70175-pubserver-perjob-leak``,
``dwoz/fix/70175-saltevent-caller-close``,
``dwoz/fix/70175-receive-path-saltevent`` and to shutdown-path fix saltstack#70206.
The ``master`` (Potassium) branch drops the ``close()`` fallback and
requires explicit ``close()`` / context-manager use; the loud
``ResourceWarning`` here is the migration signal for that change.

Regression tests:

* tests/pytests/unit/utils/test_asynchronous.py::test_syncwrapper_del_safety_net_calls_close_70175
* tests/pytests/unit/transport/test_tcp.py::test_publish_server_del_safety_net_calls_close_70175
* tests/pytests/unit/transport/test_tcp.py::test_tcppubserverpublisher_del_safety_net_calls_close_70175

Each test:
  1. Instantiates the class without ``with``, drops the reference, forces GC
  2. Asserts the ``ResourceWarning`` still fires (behavior preserved)
  3. Asserts a class-appropriate observable that only ``close()`` would
     set (asyncio_loop.is_closed() for SyncWrapper; sub-resource
     ``close()`` mock calls for PublishServer; stream+socket close for
     _TCPPubServerPublisher)

All three tests fail on pre-patch origin/3008.x with only the
``ResourceWarning`` firing, and pass with the safety-net restored.

Refs saltstack#70175, saltstack#70206.
The __del__ safety-net close() added in this PR broke transports on
fork: EventSender (and every job child) inherits parent transports via
copy-on-write, and on GC-time __del__ the child was calling close() on
socket FDs the parent still owned, tearing down the parent's event bus.

Reproduced deterministically on Python 3.14 as
tests/pytests/unit/utils/event/test_event.py::test_event_no_timeout,
which hangs 90s and times out.  The EventSender child, on exit, GC'd
the inherited MasterEvent.subscriber SyncWrapper and closed the shared
ipc_publish_client FD; the parent's recv() then blocked forever.  Also
observed in the integration zeromq/tcp and scenarios clusters as
6-minute SIGTERMs on event-dependent cluster tests.

Fix: apply the fork-safety pattern used by
salt/transport/tcp.py::Subscriber / TCPPuller (and the sibling
transport-fork-safety branch dwoz/fix/transport-fork-safety-warn-until-
close-3008.x) to the three classes whose __del__ now calls close():

  * salt.utils.asynchronous.SyncWrapper
  * salt.transport.tcp.PublishServer
  * salt.transport.tcp._TCPPubServerPublisher

__init__ records self._creator_pid = os.getpid().  __del__ short-
circuits (no warn, no close) when os.getpid() != self._creator_pid --
the parent still owns the wrapped obj / io_loop / asyncio_loop / socket
FDs; the child must not touch them.

SyncWrapper.__del__ uses self.__dict__.get(...) rather than getattr()
for the probes: __getattr__ delegates to self.obj, so a partially-
initialized instance would recurse infinitely through __getattr__ while
the finalizer is running.

Regression tests added, one per class:

  * test_syncwrapper_del_forked_child_does_not_touch_parent_resources_70175
  * test_publish_server_del_forked_child_does_not_close_parent_fds_70175
  * test_tcppubserverpublisher_del_forked_child_does_not_close_parent_fd_70175

Each simulates os.getpid() returning creator_pid + 1 (as it would in
a fork() child) and asserts (a) no ResourceWarning fires and (b) no
close() call reaches the sub-resources.
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.

2 participants