Skip to content

Commit 9dddcad

Browse files
author
Steven Silvester
committed
add handling of pending kernels
1 parent f453b51 commit 9dddcad

File tree

3 files changed

+53
-20
lines changed

3 files changed

+53
-20
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ jobs:
112112
pip freeze
113113
114114
- name: Check types
115+
if: ${{ matrix.python-version != '3.6' }}
115116
run: mypy jupyter_client --exclude '\/tests|kernelspecapp|ioloop|runapp' --install-types --non-interactive
116117

117118
- name: Run the tests

jupyter_client/manager.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import sys
99
import typing as t
1010
import uuid
11+
from asyncio.futures import Future
12+
from concurrent.futures import Future as CFuture
1113
from contextlib import contextmanager
1214
from enum import Enum
1315

@@ -58,6 +60,11 @@ class KernelManager(ConnectionFileMixin):
5860
def __init__(self, *args, **kwargs):
5961
super().__init__(**kwargs)
6062
self._shutdown_status = _ShutdownStatus.Unset
63+
try:
64+
self._ready = Future()
65+
except RuntimeError:
66+
# No event loop running, use concurrent future
67+
self._ready = CFuture()
6168

6269
_created_context: Bool = Bool(False)
6370

@@ -139,6 +146,11 @@ def kernel_spec(self) -> t.Optional[kernelspec.KernelSpec]:
139146
def _default_cache_ports(self) -> bool:
140147
return self.transport == "tcp"
141148

149+
@property
150+
def ready(self) -> Future:
151+
"""A future that resolves when the kernel process has started for the first time"""
152+
return self._ready
153+
142154
@property
143155
def ipykernel(self) -> bool:
144156
return self.kernel_name in {"python", "python2", "python3"}
@@ -329,12 +341,22 @@ async def _async_start_kernel(self, **kw):
329341
keyword arguments that are passed down to build the kernel_cmd
330342
and launching the kernel (e.g. Popen kwargs).
331343
"""
332-
kernel_cmd, kw = await ensure_async(self.pre_start_kernel(**kw))
344+
done = self._ready.done()
345+
346+
try:
347+
kernel_cmd, kw = await ensure_async(self.pre_start_kernel(**kw))
348+
349+
# launch the kernel subprocess
350+
self.log.debug("Starting kernel: %s", kernel_cmd)
351+
await ensure_async(self._launch_kernel(kernel_cmd, **kw))
352+
await ensure_async(self.post_start_kernel(**kw))
353+
if not done:
354+
self._ready.set_result(None)
333355

334-
# launch the kernel subprocess
335-
self.log.debug("Starting kernel: %s", kernel_cmd)
336-
await ensure_async(self._launch_kernel(kernel_cmd, **kw))
337-
await ensure_async(self.post_start_kernel(**kw))
356+
except Exception as e:
357+
if not done:
358+
self._ready.set_exception(e)
359+
raise e
338360

339361
start_kernel = run_sync(_async_start_kernel)
340362

@@ -471,8 +493,8 @@ async def _async_restart_kernel(self, now: bool = False, newports: bool = False,
471493
Any options specified here will overwrite those used to launch the
472494
kernel.
473495
"""
474-
if self._launch_args is None:
475-
raise RuntimeError("Cannot restart the kernel. " "No previous call to 'start_kernel'.")
496+
if not self._ready.done():
497+
raise RuntimeError("Cannot restart the kernel. " "Kernel has been not fully started.")
476498
else:
477499
# Stop currently running kernel.
478500
await ensure_async(self.shutdown_kernel(now=now, restart=True))

jupyter_client/multikernelmanager.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,23 @@ class MultiKernelManager(LoggingConfigurable):
5252
"""A class for managing multiple kernels."""
5353

5454
default_kernel_name = Unicode(
55-
NATIVE_KERNEL_NAME, config=True, help="The name of the default kernel to start"
56-
)
55+
NATIVE_KERNEL_NAME, help="The name of the default kernel to start"
56+
).tag(config=True)
5757

5858
kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
5959

6060
kernel_manager_class = DottedObjectName(
6161
"jupyter_client.ioloop.IOLoopKernelManager",
62-
config=True,
6362
help="""The kernel manager class. This is configurable to allow
6463
subclassing of the KernelManager for customized behavior.
6564
""",
66-
)
65+
).tag(config=True)
66+
67+
use_pending_kernels = Bool(
68+
False,
69+
help="""Whether to make kernels available before the process has started. The
70+
kernel has a `.ready` future which can be awaited before connecting""",
71+
).tag(config=True)
6772

6873
@observe("kernel_manager_class")
6974
def _kernel_manager_class_changed(self, change):
@@ -91,9 +96,8 @@ def create_kernel_manager(*args, **kwargs) -> KernelManager:
9196

9297
shared_context = Bool(
9398
True,
94-
config=True,
9599
help="Share a single zmq.Context to talk to all my kernels",
96-
)
100+
).tag(config=True)
97101

98102
_created_context = Bool(False)
99103

@@ -182,12 +186,18 @@ async def _async_start_kernel(self, kernel_name: t.Optional[str] = None, **kwarg
182186
)
183187
)
184188
kwargs['kernel_id'] = kernel_id # Make kernel_id available to manager and provisioner
185-
fut = asyncio.ensure_future(
186-
self._add_kernel_when_ready(kernel_id, km, ensure_async(km.start_kernel(**kwargs)))
187-
)
188-
self._starting_kernels[kernel_id] = fut
189-
await fut
190-
del self._starting_kernels[kernel_id]
189+
190+
starter = ensure_async(km.start_kernel(**kwargs))
191+
192+
if self.use_pending_kernels:
193+
asyncio.create_task(starter)
194+
self._kernels[kernel_id] = km
195+
else:
196+
fut = asyncio.ensure_future(self._add_kernel_when_ready(kernel_id, km, starter))
197+
self._starting_kernels[kernel_id] = fut
198+
await fut
199+
del self._starting_kernels[kernel_id]
200+
191201
return kernel_id
192202

193203
start_kernel = run_sync(_async_start_kernel)
@@ -212,7 +222,7 @@ async def _async_shutdown_kernel(
212222
self.log.info("Kernel shutdown: %s" % kernel_id)
213223

214224
km = self.get_kernel(kernel_id)
215-
225+
await km.ready
216226
await ensure_async(km.shutdown_kernel(now, restart))
217227
self.remove_kernel(kernel_id)
218228

0 commit comments

Comments
 (0)