From 751c7eb0cc7d252e5ee3a378056c6eee107445a1 Mon Sep 17 00:00:00 2001 From: David Leen Date: Wed, 3 Nov 2021 14:55:23 -0700 Subject: [PATCH] Fix asyncio error when opening notebooks This is a fix for https://github.com/jupyter/notebook/issues/6164 `nest_asyncio` must be applied before any async tasks have been created otherwise there could be tasks queued that are unpatched, and thus do not respect nested loops. An example of an unpatched task can be seen in the original issue: ``` ``` which originates from Tornado. A similar issue was reported in `nest-asyncio`: https://github.com/erdewit/nest_asyncio/issues/22 where the solution is to call `apply` on import so that unpatched tasks do not get created. --- notebook/notebookapp.py | 27 +++++++++++++++++++++++++++ notebook/tests/launchnotebook.py | 14 ++++++++++---- setup.py | 1 + 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 510e51240c..e07bfcf507 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -2092,7 +2092,34 @@ def _init_asyncio_patch(self): FIXME: if/when tornado supports the defaults in asyncio, remove and bump tornado requirement for py38 + + With the introduction of the async kernel, the existing sync kernel + requires the use of nested loops in order to run code synchronously. + This is done in `jupyter_client` using the helper util `run_sync`: + + ref: https://github.com/jupyter/jupyter_client/blob/f453b51eeeff9e905c583b7da3905c0e35cfbdf0/jupyter_client/utils.py#L11 + + which creates a new event loop and relies on `nest_asyncio` patching + to allow nested loops. This requires that *all* potential tasks are + patched before executing. When only some tasks are patched it leads to + the following issue: + + ref: https://github.com/jupyter/notebook/issues/6164 + + So we must call `nest_asyncio.apply()` method as early as possible. It + is preferable to do this in the consuming application rather than the + `jupyter_client` as it is a global patch and would impact all consumers + rather than just the ones that rely on synchronous kernel behavior. """ + import nest_asyncio + + try: + nest_asyncio.apply() + except RuntimeError: + # nest_asyncio requires a running loop in order to patch. + # In tests the loop may not have been created yet. + pass + if sys.platform.startswith("win") and sys.version_info >= (3, 8): import asyncio try: diff --git a/notebook/tests/launchnotebook.py b/notebook/tests/launchnotebook.py index bb5f8b7781..426ce43ffd 100644 --- a/notebook/tests/launchnotebook.py +++ b/notebook/tests/launchnotebook.py @@ -167,10 +167,16 @@ def start_thread(): token=cls.token, **bind_args ) - if 'asyncio' in sys.modules: - app._init_asyncio_patch() - import asyncio - asyncio.set_event_loop(asyncio.new_event_loop()) + if "asyncio" in sys.modules: + app._init_asyncio_patch() + import asyncio + + asyncio.set_event_loop(asyncio.new_event_loop()) + # Patch the current loop in order to match production + # behavior + import nest_asyncio + + nest_asyncio.apply() # don't register signal handler during tests app.init_signal = lambda : None # clear log handlers and propagate to root for nose to capture it diff --git a/setup.py b/setup.py index 4ae0febdc6..c54f9a1594 100755 --- a/setup.py +++ b/setup.py @@ -121,6 +121,7 @@ 'jupyter_client>=5.3.4', 'nbformat', 'nbconvert', + 'nest-asyncio>=1.5', 'ipykernel', # bless IPython kernel for now 'Send2Trash>=1.8.0', 'terminado>=0.8.3',