From 6e9dce68e8ad141fb3bdd60ac132489d0cb4099a Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:01:53 +0000 Subject: [PATCH 1/2] Add a test for closing socket on collision due to race --- tests/test_kernelapp.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_kernelapp.py b/tests/test_kernelapp.py index 44a06886a..21fbc3ad9 100644 --- a/tests/test_kernelapp.py +++ b/tests/test_kernelapp.py @@ -1,10 +1,13 @@ +import atexit import json import os +import socket import threading import time from unittest.mock import patch import pytest +import zmq from jupyter_client import KernelManager from jupyter_client.kernelspec import KernelSpecManager from jupyter_core.paths import secure_write @@ -30,6 +33,21 @@ def test_init_ipc_socket(): app.close() +def test_close_after_iopub_port_in_use(): + app = IPKernelApp(transport="tcp") + with socket.socket() as taken: + taken.bind((app.ip, 0)) + taken.listen(1) + app.iopub_port = taken.getsockname()[1] + with pytest.raises(zmq.ZMQError): + app.init_sockets() + atexit.unregister(app.close) + closing = threading.Thread(target=app.close, daemon=True) + closing.start() + closing.join(timeout=10) + assert not closing.is_alive(), "close() is still waiting for the zmq context to terminate" + + def test_blackhole(): app = IPKernelApp() app.no_stderr = True From 780c918e8596c5480a0b9659892eed2bb3522532 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:04:58 +0000 Subject: [PATCH 2/2] Fix the issue --- ipykernel/kernelapp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index 41ffbcc39..667df15d9 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -460,6 +460,10 @@ def close(self): self.log.debug("Closing iopub channel") self.iopub_thread.stop() self.iopub_thread.close() + elif self.iopub_socket is not None and not self.iopub_socket.closed: + # The iopub port could not be bound, so no IOPub thread owns the socket. + # Left open, it would block context.term() below. + self.iopub_socket.close() if self.control_thread and self.control_thread.is_alive(): self.log.debug("Closing control thread") self.control_thread.stop()