From bd7f8466c1ccdcf37df197de2abdf616dfb70701 Mon Sep 17 00:00:00 2001 From: Simon Cross Date: Wed, 16 Apr 2025 15:41:14 +0200 Subject: [PATCH 1/3] Add filelock to dependencies. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 971bcf5..c307c60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ + "filelock>=3.12.0", "jupyter_client>=6.1.12", "jupyter_core>=4.12,!=5.0.*", "nbformat>=5.1", From d055af5ca32872c6c75c9fc619be64ebeba4fb31 Mon Sep 17 00:00:00 2001 From: Simon Cross Date: Wed, 16 Apr 2025 15:42:30 +0200 Subject: [PATCH 2/3] Optionally apply a filelock around kernel start-up. --- nbclient/client.py | 52 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/nbclient/client.py b/nbclient/client.py index 936bb4a..e5700d2 100644 --- a/nbclient/client.py +++ b/nbclient/client.py @@ -14,6 +14,7 @@ from textwrap import dedent from time import monotonic +import filelock from jupyter_client.client import KernelClient from jupyter_client.manager import KernelManager from nbformat import NotebookNode @@ -57,6 +58,15 @@ def timestamp(msg: dict[str, t.Any] | None = None) -> str: return datetime.datetime.utcnow().isoformat() + "Z" +class _DummyFileLock: + """A dummy filelock.FileLock for use when locking is disabled.""" + def acquire(self): + pass + + def release(self): + pass + + class NotebookClient(LoggingConfigurable): """ Encompasses a Client for executing cells in a notebook @@ -291,6 +301,20 @@ class NotebookClient(LoggingConfigurable): config=True, klass=KernelManager, help="The kernel manager class to use." ) + setup_kernel_lock_file = Unicode( + default_value="", + help=dedent( + """ + Path of the lock file to hold while a kernel is being started. + Holding a lock prevents port clashes when starting local kernels + from multiple processes simultaneously. + + Once https://github.com/jupyter/enhancement-proposals/pull/66 a + lock will no longer be required. + """ + ), + ).tag(config=True) + on_notebook_start = Callable( default_value=None, allow_none=True, @@ -466,6 +490,10 @@ def __init__(self, nb: NotebookNode, km: KernelManager | None = None, **kw: t.An self.comm_open_handlers: dict[str, t.Any] = { "jupyter.widget": self.on_comm_open_jupyter_widget } + if self.setup_kernel_lock_file: + self._setup_kernel_lock = filelock.FileLock(self.setup_kernel_lock_file) + else: + self._setup_kernel_lock = _DummyFileLock() def reset_execution_trackers(self) -> None: """Resets any per-execution trackers.""" @@ -596,11 +624,15 @@ def setup_kernel(self, **kwargs: t.Any) -> t.Generator[None, None, None]: if self.km is None: self.km = self.create_kernel_manager() - if not self.km.has_kernel: - self.start_new_kernel(**kwargs) + self._setup_kernel_lock.acquire() + try: + if not self.km.has_kernel: + self.start_new_kernel(**kwargs) - if self.kc is None: - self.start_new_kernel_client() + if self.kc is None: + self.start_new_kernel_client() + finally: + self._setup_kernel_lock.release() try: yield @@ -644,11 +676,15 @@ def on_signal() -> None: # RuntimeError: Raised when add_signal_handler is called outside the main thread pass - if not self.km.has_kernel: - await self.async_start_new_kernel(**kwargs) + self._setup_kernel_lock.acquire() + try: + if not self.km.has_kernel: + await self.async_start_new_kernel(**kwargs) - if self.kc is None: - await self.async_start_new_kernel_client() + if self.kc is None: + await self.async_start_new_kernel_client() + finally: + self._setup_kernel_lock.release() try: yield From 678b1ccc54cf5d86d365ccdbd20d3e0a19ba449d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:52:02 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- nbclient/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nbclient/client.py b/nbclient/client.py index e5700d2..87b012a 100644 --- a/nbclient/client.py +++ b/nbclient/client.py @@ -60,6 +60,7 @@ def timestamp(msg: dict[str, t.Any] | None = None) -> str: class _DummyFileLock: """A dummy filelock.FileLock for use when locking is disabled.""" + def acquire(self): pass