diff --git a/changelog/66568.fixed.md b/changelog/66568.fixed.md new file mode 100644 index 000000000000..fbfa1aa540b0 --- /dev/null +++ b/changelog/66568.fixed.md @@ -0,0 +1 @@ +Fix closing of TCP transport channels and avoid additional errors diff --git a/salt/channel/client.py b/salt/channel/client.py index ae10d09d86b8..d93f5d4b8a9b 100644 --- a/salt/channel/client.py +++ b/salt/channel/client.py @@ -556,7 +556,10 @@ def connect_callback(self, result): # may have been restarted yield self.send_id(self.token, self._reconnected) self.connected = True - self.event.fire_event({"master": self.opts["master"]}, "__master_connected") + if self.event: + self.event.fire_event( + {"master": self.opts["master"]}, "__master_connected" + ) if self._reconnected: # On reconnects, fire a master event to notify that the minion is # available. diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index ec0941d75af6..8c1805053a32 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -235,7 +235,6 @@ def __init__(self, opts, io_loop, **kwargs): # pylint: disable=W0231 self.connected = False self._closing = False self._stream = None - self._closing = False self._closed = False self.backoff = opts.get("tcp_reconnect_backoff", 1) self.resolver = kwargs.get("resolver") @@ -1763,6 +1762,7 @@ def __init__(self, opts, io_loop, **kwargs): # pylint: disable=W0231 self._closed = False self._stream_return_running = False self._stream = None + self.task = None self.disconnect_callback = _null_callback self.connect_callback = _null_callback self.backoff = opts.get("tcp_reconnect_backoff", 1) @@ -1831,7 +1831,7 @@ async def _stream_return(self): message_id, ) except tornado.iostream.StreamClosedError as e: - log.error( + log.debug( "tcp stream to %s:%s closed, unable to recv", self.host, self.port, @@ -1873,6 +1873,8 @@ async def _stream_return(self): stream.close() unpacker = salt.utils.msgpack.Unpacker() await self.connect() + except asyncio.CancelledError: + log.debug("Stream return cancelled") self._stream_return_running = False def _message_id(self): @@ -1921,9 +1923,20 @@ async def _do_send(): def close(self): if self._closing: return + self._closing = True if self._stream is not None: self._stream.close() self._stream = None + if self.task is not None: + self.task.cancel() + # Wait for the task to finish via asyncio + group = asyncio.gather(self.task) + try: + self.task.get_loop().run_until_complete(group) + except RuntimeError: + # Ignore event loop was already running message + pass + self.task = None def __enter__(self): return self diff --git a/tests/pytests/functional/transport/server/test_request_server.py b/tests/pytests/functional/transport/server/test_request_server.py index d2f0363791c0..ee4f34c294e4 100644 --- a/tests/pytests/functional/transport/server/test_request_server.py +++ b/tests/pytests/functional/transport/server/test_request_server.py @@ -44,10 +44,16 @@ async def handler(message): try: ret = await req_client.send({"req": "test"}) + if transport == "tcp": + assert req_client.task is not None assert [reqmsg] == requests assert repmsg == ret finally: req_client.close() + if transport == "tcp": + # Ensure that issue 68277 is fixed + assert req_client.task is None or req_client.task.done() + assert req_client._closing req_server.close() # Yield to loop in order to allow background close methods to finish.