Skip to content

Commit cd14b0e

Browse files
authored
Fix channel cleanup by closing streams before stopping ioloop thread (#1089)
* Fix channel cleanup by closing streams before stopping ioloop thread The change in PR #1076 added 'finally: loop.close()' to IOLoopThread.run(), which closes the asyncio event loop before ZMQ streams have been properly unregistered. This caused errors during teardown when downstream projects like qtconsole tried to check if channels were closed. The fix ensures that channel streams are explicitly closed (via close()) while the ioloop is still running, before calling stop_channels() on the parent class and stopping the ioloop thread. This allows the ZMQ streams to be properly unregistered from the event loop before it's closed. * Fix _exiting to be instance variable, not shared class variable The _exiting flag was defined as a class variable, meaning when one IOLoopThread called stop() and set _exiting = True, it affected ALL IOLoopThread instances. This caused subsequent kernels to fail to start because _async_run() would immediately exit. By initializing self._exiting = False in __init__, each IOLoopThread instance now has its own exit flag. The class-level _exiting is still used by _notice_exit() for interpreter shutdown cleanup.
1 parent 49a4376 commit cd14b0e

File tree

1 file changed

+20
-0
lines changed

1 file changed

+20
-0
lines changed

jupyter_client/threaded.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ def __init__(self) -> None:
230230
super().__init__()
231231
self.daemon = True
232232

233+
# Instance variable to track exit state for this specific thread.
234+
# The class variable _exiting is used by _notice_exit for interpreter shutdown.
235+
# Without this instance variable, stopping one IOLoopThread sets the class-level
236+
# _exiting = True, causing all subsequent IOLoopThread instances to exit immediately
237+
# in _async_run(). This breaks sequential kernel usage (e.g., qtconsole tests).
238+
self._exiting = False
239+
233240
@staticmethod
234241
@atexit.register
235242
def _notice_exit() -> None:
@@ -333,6 +340,19 @@ def _check_kernel_info_reply(self, msg: dict[str, Any]) -> None:
333340

334341
def stop_channels(self) -> None:
335342
"""Stop the channels on the client."""
343+
# Close channel streams while ioloop is still running
344+
# This must happen before stopping the ioloop thread, otherwise
345+
# the ZMQ streams can't be properly unregistered from the event loop
346+
if self.ioloop_thread and self.ioloop_thread.is_alive():
347+
if self._shell_channel is not None:
348+
self._shell_channel.close()
349+
if self._iopub_channel is not None:
350+
self._iopub_channel.close()
351+
if self._stdin_channel is not None:
352+
self._stdin_channel.close()
353+
if self._control_channel is not None:
354+
self._control_channel.close()
355+
336356
super().stop_channels()
337357
if self.ioloop_thread and self.ioloop_thread.is_alive():
338358
self.ioloop_thread.stop()

0 commit comments

Comments
 (0)