From 1687f8ce80e488ed51c751e3f47acce2d9311741 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 7 May 2026 10:52:13 +0200 Subject: [PATCH] Fix ENOTSOCK traceback when iopub socket closes during shutdown Likely fix nbclient downstream test. After a control handler completes, process_control unconditionally publishes an "idle" status. During shutdown this races with IOPubThread.close(): the IO thread has been joined, so _really_send runs on the caller's thread while close() runs on another, and the `if self.closed` check is TOCTOU with the subsequent send_multipart. Catch the resulting ZMQError(ENOTSOCK) (and AttributeError if self.socket has flipped to None) in _really_send and log at debug level instead of letting tornado log an uncaught-callback traceback. --- ipykernel/iostream.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ipykernel/iostream.py b/ipykernel/iostream.py index 33213d167..2c1094f35 100644 --- a/ipykernel/iostream.py +++ b/ipykernel/iostream.py @@ -7,6 +7,7 @@ import atexit import contextvars import io +import logging import os import sys import threading @@ -33,6 +34,8 @@ PIPE_BUFFER_SIZE = 1000 +logger = logging.getLogger(__name__) + # ----------------------------------------------------------------------------- # IO classes # ----------------------------------------------------------------------------- @@ -355,8 +358,20 @@ def _really_send(self, msg, *args, **kwargs): mp_mode = self._check_mp_mode() if mp_mode != CHILD: - # we are master, do a regular send - self.socket.send_multipart(msg, *args, **kwargs) + # we are master, do a regular send. + # The closed check above is racy: once the IO thread has been + # joined, _really_send runs on the caller's thread while close() + # may run concurrently on another, so the socket can be closed + # (or become None) between the check and the send. Swallow that + # specific case rather than logging a noisy traceback during + # shutdown. + try: + self.socket.send_multipart(msg, *args, **kwargs) + except (AttributeError, zmq.error.ZMQError) as e: + if isinstance(e, AttributeError) or e.errno == zmq.ENOTSOCK: + logger.debug("IOPub socket closed during send (likely shutdown): %s", e) + return + raise else: # we are a child, pipe to master # new context/socket for every pipe-out