Skip to content

Commit 3c6e373

Browse files
Steven Silvestergithub-actions[bot]
authored andcommitted
wip
1 parent 09967c1 commit 3c6e373

File tree

6 files changed

+157
-41
lines changed

6 files changed

+157
-41
lines changed

jupyter_client/manager.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,14 @@ async def _async_start_kernel(self, **kw):
351351
await ensure_async(self._launch_kernel(kernel_cmd, **kw))
352352
await ensure_async(self.post_start_kernel(**kw))
353353
if not done:
354+
# Add a small sleep to ensure tests can capture the state before done
355+
await asyncio.sleep(0.01)
354356
self._ready.set_result(None)
355357

356358
except Exception as e:
357359
if not done:
358360
self._ready.set_exception(e)
361+
self.log.exception(self._ready.exception())
359362
raise e
360363

361364
start_kernel = run_sync(_async_start_kernel)
@@ -449,6 +452,10 @@ async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False)
449452
Will this kernel be restarted after it is shutdown. When this
450453
is True, connection files will not be cleaned up.
451454
"""
455+
# Shutdown is a no-op for a kernel that had a failed startup
456+
if self._ready.exception():
457+
return
458+
452459
self.shutting_down = True # Used by restarter to prevent race condition
453460
# Stop monitoring for restarting while we shutdown.
454461
self.stop_restarter()

jupyter_client/multikernelmanager.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,6 @@ class MultiKernelManager(LoggingConfigurable):
6464
""",
6565
).tag(config=True)
6666

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)
72-
7367
@observe("kernel_manager_class")
7468
def _kernel_manager_class_changed(self, change):
7569
self.kernel_manager_factory = self._create_kernel_manager_factory()
@@ -167,8 +161,11 @@ def pre_start_kernel(
167161
async def _add_kernel_when_ready(
168162
self, kernel_id: str, km: KernelManager, kernel_awaitable: t.Awaitable
169163
) -> None:
170-
await kernel_awaitable
171-
self._kernels[kernel_id] = km
164+
try:
165+
await kernel_awaitable
166+
self._kernels[kernel_id] = km
167+
finally:
168+
self._starting_kernels.pop(kernel_id, None)
172169

173170
async def _async_start_kernel(self, kernel_name: t.Optional[str] = None, **kwargs) -> str:
174171
"""Start a new kernel.
@@ -188,15 +185,13 @@ async def _async_start_kernel(self, kernel_name: t.Optional[str] = None, **kwarg
188185
kwargs['kernel_id'] = kernel_id # Make kernel_id available to manager and provisioner
189186

190187
starter = ensure_async(km.start_kernel(**kwargs))
188+
fut = asyncio.ensure_future(self._add_kernel_when_ready(kernel_id, km, starter))
189+
self._starting_kernels[kernel_id] = fut
191190

192-
if self.use_pending_kernels:
193-
asyncio.create_task(starter)
191+
if getattr(self, 'use_pending_kernels', False):
194192
self._kernels[kernel_id] = km
195193
else:
196-
fut = asyncio.ensure_future(self._add_kernel_when_ready(kernel_id, km, starter))
197-
self._starting_kernels[kernel_id] = fut
198194
await fut
199-
del self._starting_kernels[kernel_id]
200195

201196
return kernel_id
202197

@@ -220,9 +215,13 @@ async def _async_shutdown_kernel(
220215
Will the kernel be restarted?
221216
"""
222217
self.log.info("Kernel shutdown: %s" % kernel_id)
223-
218+
if kernel_id in self._starting_kernels:
219+
try:
220+
await self._starting_kernels[kernel_id]
221+
except Exception:
222+
self.remove_kernel(kernel_id)
223+
return
224224
km = self.get_kernel(kernel_id)
225-
await km.ready
226225
await ensure_async(km.shutdown_kernel(now, restart))
227226
self.remove_kernel(kernel_id)
228227

@@ -256,18 +255,11 @@ def remove_kernel(self, kernel_id: str) -> KernelManager:
256255
"""
257256
return self._kernels.pop(kernel_id, None)
258257

259-
async def _shutdown_starting_kernel(self, kid: str, now: bool) -> None:
260-
if kid in self._starting_kernels:
261-
await self._starting_kernels[kid]
262-
await ensure_async(self.shutdown_kernel(kid, now=now))
263-
264258
async def _async_shutdown_all(self, now: bool = False) -> None:
265259
"""Shutdown all kernels."""
266260
kids = self.list_kernel_ids()
267-
futs = [ensure_async(self.shutdown_kernel(kid, now=now)) for kid in kids]
268-
futs += [
269-
self._shutdown_starting_kernel(kid, now=now) for kid in self._starting_kernels.keys()
270-
]
261+
kids += list(self._starting_kernels)
262+
futs = [ensure_async(self.shutdown_kernel(kid, now=now)) for kid in set(kids)]
271263
await asyncio.gather(*futs)
272264

273265
shutdown_all = run_sync(_async_shutdown_all)
@@ -476,6 +468,12 @@ class AsyncMultiKernelManager(MultiKernelManager):
476468
""",
477469
)
478470

471+
use_pending_kernels = Bool(
472+
False,
473+
help="""Whether to make kernels available before the process has started. The
474+
kernel has a `.ready` future which can be awaited before connecting""",
475+
).tag(config=True)
476+
479477
start_kernel = MultiKernelManager._async_start_kernel
480478
shutdown_kernel = MultiKernelManager._async_shutdown_kernel
481479
shutdown_all = MultiKernelManager._async_shutdown_all

jupyter_client/tests/test_kernelmanager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ class TestKernelManager:
188188
def test_lifecycle(self, km):
189189
km.start_kernel(stdout=PIPE, stderr=PIPE)
190190
assert km.is_alive()
191+
is_done = km.ready.done()
192+
assert is_done
191193
km.restart_kernel(now=True)
192194
assert km.is_alive()
193195
km.interrupt_kernel()
@@ -439,6 +441,8 @@ async def test_lifecycle(self, async_km):
439441
await async_km.start_kernel(stdout=PIPE, stderr=PIPE)
440442
is_alive = await async_km.is_alive()
441443
assert is_alive
444+
is_ready = async_km.ready.done()
445+
assert is_ready
442446
await async_km.restart_kernel(now=True)
443447
is_alive = await async_km.is_alive()
444448
assert is_alive

jupyter_client/tests/test_kernelspec.py

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,18 @@
1818
import pytest
1919
from jupyter_core import paths
2020

21+
from .utils import install_kernel
22+
from .utils import sample_kernel_json
2123
from .utils import test_env
2224
from jupyter_client import kernelspec
2325

24-
sample_kernel_json = {
25-
"argv": ["cat", "{connection_file}"],
26-
"display_name": "Test kernel",
27-
}
28-
2926

3027
class KernelSpecTests(unittest.TestCase):
31-
def _install_sample_kernel(self, kernels_dir):
32-
"""install a sample kernel in a kernels directory"""
33-
sample_kernel_dir = pjoin(kernels_dir, "sample")
34-
os.makedirs(sample_kernel_dir)
35-
json_file = pjoin(sample_kernel_dir, "kernel.json")
36-
with open(json_file, "w") as f:
37-
json.dump(sample_kernel_json, f)
38-
return sample_kernel_dir
39-
4028
def setUp(self):
4129
self.env_patch = test_env()
4230
self.env_patch.start()
43-
self.sample_kernel_dir = self._install_sample_kernel(
44-
pjoin(paths.jupyter_data_dir(), "kernels")
31+
self.sample_kernel_dir = install_kernel(
32+
pjoin(paths.jupyter_data_dir(), "kernels"), name="sample"
4533
)
4634

4735
self.ksm = kernelspec.KernelSpecManager()
@@ -87,7 +75,7 @@ def test_find_all_specs(self):
8775
def test_kernel_spec_priority(self):
8876
td = TemporaryDirectory()
8977
self.addCleanup(td.cleanup)
90-
sample_kernel = self._install_sample_kernel(td.name)
78+
sample_kernel = install_kernel(td.name, name="sample")
9179
self.ksm.kernel_dirs.append(td.name)
9280
kernels = self.ksm.find_kernel_specs()
9381
self.assertEqual(kernels["sample"], self.sample_kernel_dir)

jupyter_client/tests/test_multikernelmanager.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
"""Tests for the notebook kernel and session manager."""
22
import asyncio
33
import concurrent.futures
4+
import os
45
import sys
56
import uuid
67
from subprocess import PIPE
78
from unittest import TestCase
89

910
import pytest
11+
from jupyter_core import paths
1012
from tornado.testing import AsyncTestCase
1113
from tornado.testing import gen_test
1214
from traitlets.config.loader import Config
1315

1416
from ..localinterfaces import localhost
1517
from .utils import AsyncKMSubclass
1618
from .utils import AsyncMKMSubclass
19+
from .utils import install_kernel
1720
from .utils import skip_win32
1821
from .utils import SyncKMSubclass
1922
from .utils import SyncMKMSubclass
23+
from .utils import test_env
2024
from jupyter_client import AsyncKernelManager
2125
from jupyter_client import KernelManager
2226
from jupyter_client.multikernelmanager import AsyncMultiKernelManager
@@ -26,6 +30,10 @@
2630

2731

2832
class TestKernelManager(TestCase):
33+
def setUp(self):
34+
self.env_patch = test_env()
35+
self.env_patch.start()
36+
super().setUp()
2937

3038
# static so picklable for multiprocessing on Windows
3139
@staticmethod
@@ -58,6 +66,7 @@ def _run_lifecycle(km, test_kid=None):
5866
else:
5967
kid = km.start_kernel(stdout=PIPE, stderr=PIPE)
6068
assert km.is_alive(kid)
69+
assert km.get_kernel(kid).ready.done()
6170
assert kid in km
6271
assert kid in km.list_kernel_ids()
6372
assert len(km) == 1, f"{len(km)} != {1}"
@@ -220,6 +229,10 @@ def test_subclass_callables(self):
220229

221230

222231
class TestAsyncKernelManager(AsyncTestCase):
232+
def setUp(self):
233+
self.env_patch = test_env()
234+
self.env_patch.start()
235+
super().setUp()
223236

224237
# static so picklable for multiprocessing on Windows
225238
@staticmethod
@@ -243,6 +256,13 @@ def _get_ipc_km():
243256
km = AsyncMultiKernelManager(config=c)
244257
return km
245258

259+
@staticmethod
260+
def _get_pending_kernels_km():
261+
c = Config()
262+
c.AsyncMultiKernelManager.use_pending_kernels = True
263+
km = AsyncMultiKernelManager(config=c)
264+
return km
265+
246266
# static so picklable for multiprocessing on Windows
247267
@staticmethod
248268
async def _run_lifecycle(km, test_kid=None):
@@ -334,6 +354,57 @@ async def test_shutdown_all_while_starting(self):
334354
# shutdown again is okay, because we have no kernels
335355
await km.shutdown_all()
336356

357+
@gen_test
358+
async def test_use_pending_kernels(self):
359+
km = self._get_pending_kernels_km()
360+
kid = await km.start_kernel(stdout=PIPE, stderr=PIPE)
361+
kernel = km.get_kernel(kid)
362+
assert not kernel.ready.done()
363+
assert kid in km
364+
assert kid in km.list_kernel_ids()
365+
assert len(km) == 1, f"{len(km)} != {1}"
366+
await kernel.ready
367+
await km.restart_kernel(kid, now=True)
368+
assert await km.is_alive(kid)
369+
assert kid in km.list_kernel_ids()
370+
await km.interrupt_kernel(kid)
371+
k = km.get_kernel(kid)
372+
assert isinstance(k, AsyncKernelManager)
373+
await km.shutdown_kernel(kid, now=True)
374+
assert kid not in km, f"{kid} not in {km}"
375+
376+
@gen_test
377+
async def test_use_pending_kernels_early_restart(self):
378+
km = self._get_pending_kernels_km()
379+
kid = await km.start_kernel(stdout=PIPE, stderr=PIPE)
380+
kernel = km.get_kernel(kid)
381+
assert not kernel.ready.done()
382+
with pytest.raises(RuntimeError):
383+
await km.restart_kernel(kid, now=True)
384+
await kernel.ready
385+
await km.shutdown_kernel(kid, now=True)
386+
assert kid not in km, f"{kid} not in {km}"
387+
388+
@gen_test
389+
async def test_use_pending_kernels_early_shutdown(self):
390+
km = self._get_pending_kernels_km()
391+
kid = await km.start_kernel(stdout=PIPE, stderr=PIPE)
392+
kernel = km.get_kernel(kid)
393+
assert not kernel.ready.done()
394+
await km.shutdown_kernel(kid, now=True)
395+
assert kid not in km, f"{kid} not in {km}"
396+
397+
@gen_test
398+
async def test_use_pending_kernels_early_interrupt(self):
399+
km = self._get_pending_kernels_km()
400+
kid = await km.start_kernel(stdout=PIPE, stderr=PIPE)
401+
kernel = km.get_kernel(kid)
402+
assert not kernel.ready.done()
403+
with pytest.raises(RuntimeError):
404+
await km.interrupt_kernel(kid)
405+
await km.shutdown_kernel(kid, now=True)
406+
assert kid not in km, f"{kid} not in {km}"
407+
337408
@gen_test
338409
async def test_tcp_cinfo(self):
339410
km = self._get_tcp_km()
@@ -466,3 +537,30 @@ async def test_subclass_callables(self):
466537
assert mkm.call_count("cleanup_resources") == 0
467538

468539
assert kid not in mkm, f"{kid} not in {mkm}"
540+
541+
@gen_test
542+
async def test_bad_kernelspec(self):
543+
km = self._get_tcp_km()
544+
install_kernel(
545+
os.path.join(paths.jupyter_data_dir(), "kernels"),
546+
argv=["non_existent_executable"],
547+
name="bad",
548+
)
549+
with pytest.raises(FileNotFoundError):
550+
await km.start_kernel(kernel_name="bad", stdout=PIPE, stderr=PIPE)
551+
552+
@gen_test
553+
async def test_bad_kernelspec_pending(self):
554+
km = self._get_pending_kernels_km()
555+
install_kernel(
556+
os.path.join(paths.jupyter_data_dir(), "kernels"),
557+
argv=["non_existent_executable"],
558+
name="bad",
559+
)
560+
kernel_id = await km.start_kernel(kernel_name="bad", stdout=PIPE, stderr=PIPE)
561+
assert kernel_id in km._starting_kernels
562+
with pytest.raises(FileNotFoundError):
563+
await km.get_kernel(kernel_id).ready
564+
assert kernel_id in km.list_kernel_ids()
565+
await km.shutdown_kernel(kernel_id)
566+
assert kernel_id not in km.list_kernel_ids()

jupyter_client/tests/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Testing utils for jupyter_client tests
22
33
"""
4+
import json
45
import os
56
import sys
67
from tempfile import TemporaryDirectory
@@ -19,6 +20,26 @@
1920
skip_win32 = pytest.mark.skipif(sys.platform.startswith("win"), reason="Windows")
2021

2122

23+
sample_kernel_json = {
24+
"argv": ["cat", "{connection_file}"],
25+
"display_name": "Test kernel",
26+
}
27+
28+
29+
def install_kernel(kernels_dir, argv=None, name="test", display_name=None):
30+
"""install a kernel in a kernels directory"""
31+
kernel_dir = pjoin(kernels_dir, name)
32+
os.makedirs(kernel_dir)
33+
kernel_json = {
34+
"argv": argv or sample_kernel_json["argv"],
35+
"display_name": display_name or sample_kernel_json["display_name"],
36+
}
37+
json_file = pjoin(kernel_dir, "kernel.json")
38+
with open(json_file, "w") as f:
39+
json.dump(kernel_json, f)
40+
return kernel_dir
41+
42+
2243
class test_env(object):
2344
"""Set Jupyter path variables to a temporary directory
2445

0 commit comments

Comments
 (0)