diff --git a/ipykernel/iostream.py b/ipykernel/iostream.py index 2c1094f35..a5b5b6c0b 100644 --- a/ipykernel/iostream.py +++ b/ipykernel/iostream.py @@ -22,9 +22,10 @@ import zmq from jupyter_client.session import extract_header -from tornado.ioloop import IOLoop from zmq.eventloop.zmqstream import ZMQStream +from .thread import make_selector_io_loop + # ----------------------------------------------------------------------------- # Globals # ----------------------------------------------------------------------------- @@ -67,7 +68,7 @@ def __init__(self, socket, pipe=False, session=False): self.background_socket = BackgroundSocket(self) self._master_pid = os.getpid() self._pipe_flag = pipe - self.io_loop = IOLoop(make_current=False) + self.io_loop = make_selector_io_loop() if pipe: self._setup_pipe_in() self._local = threading.local() diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index 9413b86d6..b6e8b59a1 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -692,43 +692,6 @@ def configure_tornado_logger(self): handler.setFormatter(formatter) logger.addHandler(handler) - def _init_asyncio_patch(self): - """set default asyncio policy to be compatible with tornado - - Tornado 6 (at least) is not compatible with the default - asyncio implementation on Windows - - Pick the older SelectorEventLoopPolicy on Windows - if the known-incompatible default policy is in use. - - Support for Proactor via a background thread is available in tornado 6.1, - but it is still preferable to run the Selector in the main thread - instead of the background. - - do this as early as possible to make it a low priority and overridable - - ref: https://github.com/tornadoweb/tornado/issues/2608 - - FIXME: if/when tornado supports the defaults in asyncio without threads, - remove and bump tornado requirement for py38. - Most likely, this will mean a new Python version - where asyncio.ProactorEventLoop supports add_reader and friends. - - """ - if sys.platform.startswith("win"): - import asyncio - - try: - from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy - except ImportError: - pass - # not affected - else: - if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy: - # WindowsProactorEventLoopPolicy is not compatible with tornado 6 - # fallback to the pre-3.8 default of Selector - asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - def init_pdb(self): """Replace pdb with IPython's version that is interruptible. @@ -748,7 +711,6 @@ def init_pdb(self): @catch_config_error def initialize(self, argv=None): """Initialize the application.""" - self._init_asyncio_patch() super().initialize(argv) if self.subapp is not None: return diff --git a/ipykernel/thread.py b/ipykernel/thread.py index 3e1d4f07a..bb6764984 100644 --- a/ipykernel/thread.py +++ b/ipykernel/thread.py @@ -1,5 +1,7 @@ """Base class for threads.""" +import asyncio +import sys from threading import Thread from tornado.ioloop import IOLoop @@ -8,13 +10,35 @@ SHELL_CHANNEL_THREAD_NAME = "Shell channel" +def make_selector_io_loop() -> IOLoop: + """Create a non-current tornado ``IOLoop`` for an ipykernel service thread. + + ipykernel runs its service channels -- control, IOPub, the shell channel and + subshells -- on dedicated event loops in background threads. The process-wide + asyncio loop on Windows is a ``ProactorEventLoop`` (so the main user-code loop + can spawn asyncio subprocesses, see #1468/#1469), and Proactor has no native + ``add_reader``. Tornado therefore drives a Proactor loop's zmq sockets through + a helper "Tornado selector" thread. When a debugger suspends every thread at a + breakpoint on Python >= 3.12 (``sys.monitoring``), that un-exempt helper thread + freezes mid-wake and deadlocks the control/debug read path (#1469). + + These service loops never need Proactor's subprocess support, so we keep them + on a ``SelectorEventLoop``: it implements ``add_reader`` natively and needs no + helper thread. Only the main/user-code loop stays on Proactor. On non-Windows + platforms the default loop is already selector-based, so this is a no-op there. + """ + if sys.platform == "win32": + return IOLoop(make_current=False, asyncio_loop=asyncio.SelectorEventLoop()) + return IOLoop(make_current=False) + + class BaseThread(Thread): """Base class for threads.""" def __init__(self, **kwargs): """Initialize the thread.""" super().__init__(**kwargs) - self.io_loop = IOLoop(make_current=False) + self.io_loop = make_selector_io_loop() self.pydev_do_not_trace = True self.is_pydev_daemon_thread = True diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index a92bf7e49..67395e4e9 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -518,6 +518,14 @@ class ZMQInteractiveShell(InteractiveShell): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Suppress Trio's signal handling warning on Windows with ProactorEventLoop + # This occurs when Trio is imported and finds signal handling already taken by Proactor + warnings.filterwarnings( + "ignore", + message=".*Trio's signal handling code might have collided.*", + category=RuntimeWarning, + ) + # tqdm has an incorrect detection of ZMQInteractiveShell when launch via # a scheduler that bypass IPKernelApp Think of JupyterHub cluster # spawners and co. as of end of Feb 2025, the maintainer has been diff --git a/tests/test_async.py b/tests/test_async.py index 4e9b9ad46..2c5b21ea9 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -35,7 +35,7 @@ def test_async_interrupt(asynclib, request): __import__(asynclib) except ImportError: pytest.skip("Requires %s" % asynclib) - request.addfinalizer(lambda: execute("%autoawait asyncio", KC)) + request.addfinalizer(lambda: execute("%autoawait " + asynclib, KC)) flush_channels(KC) msg_id, content = execute("%autoawait " + asynclib, KC)