Skip to content

Commit 2e31f90

Browse files
authored
Backport PR #792 on branch 1.x (Centralize app cleanup) (#819)
1 parent 6791cf9 commit 2e31f90

File tree

9 files changed

+71
-161
lines changed

9 files changed

+71
-161
lines changed

.github/workflows/python-tests.yml

+2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ jobs:
137137
steps:
138138
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
139139
- uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1
140+
with:
141+
test_command: pytest --vv || pytest -vv --lf
140142

141143
python_tests_check: # This job does nothing and is only used for the branch protection
142144
if: always()

jupyter_server/pytest_plugin.py

+7-39
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,7 @@ def jp_ensure_app_fixture(request):
299299
@pytest.fixture(scope="function")
300300
def jp_serverapp(jp_ensure_app_fixture, jp_server_config, jp_argv, jp_configurable_serverapp):
301301
"""Starts a Jupyter Server instance based on the established configuration values."""
302-
app = jp_configurable_serverapp(config=jp_server_config, argv=jp_argv)
303-
yield app
304-
app.remove_server_info_file()
305-
app.remove_browser_open_files()
302+
return jp_configurable_serverapp(config=jp_server_config, argv=jp_argv)
306303

307304

308305
@pytest.fixture
@@ -463,48 +460,19 @@ def inner(nbpath):
463460

464461

465462
@pytest.fixture(autouse=True)
466-
def jp_server_cleanup():
463+
def jp_server_cleanup(io_loop):
467464
yield
465+
app: ServerApp = ServerApp.instance()
466+
loop = io_loop.asyncio_loop
467+
loop.run_until_complete(app._cleanup())
468468
ServerApp.clear_instance()
469469

470470

471471
@pytest.fixture
472472
def jp_cleanup_subprocesses(jp_serverapp):
473-
"""Clean up subprocesses started by a Jupyter Server, i.e. kernels and terminal."""
473+
"""DEPRECATED: The jp_server_cleanup fixture automatically cleans up the singleton ServerApp class"""
474474

475475
async def _():
476-
terminal_cleanup = jp_serverapp.web_app.settings["terminal_manager"].terminate_all
477-
kernel_cleanup = jp_serverapp.kernel_manager.shutdown_all
478-
479-
async def kernel_cleanup_steps():
480-
# Try a graceful shutdown with a timeout
481-
try:
482-
await asyncio.wait_for(kernel_cleanup(), timeout=15.0)
483-
except asyncio.TimeoutError:
484-
# Now force a shutdown
485-
try:
486-
await asyncio.wait_for(kernel_cleanup(now=True), timeout=15.0)
487-
except asyncio.TimeoutError:
488-
print(Exception("Kernel never shutdown!"))
489-
except Exception as e:
490-
print(e)
491-
492-
if asyncio.iscoroutinefunction(terminal_cleanup):
493-
try:
494-
await terminal_cleanup()
495-
except Exception as e:
496-
print(e)
497-
else:
498-
try:
499-
await terminal_cleanup()
500-
except Exception as e:
501-
print(e)
502-
if asyncio.iscoroutinefunction(kernel_cleanup):
503-
await kernel_cleanup_steps()
504-
else:
505-
try:
506-
kernel_cleanup()
507-
except Exception as e:
508-
print(e)
476+
pass
509477

510478
return _

jupyter_server/serverapp.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -2427,6 +2427,8 @@ async def cleanup_kernels(self):
24272427
The kernels will shutdown themselves when this process no longer exists,
24282428
but explicit shutdown allows the KernelManagers to cleanup the connection files.
24292429
"""
2430+
if not getattr(self, "kernel_manager", None):
2431+
return
24302432
n_kernels = len(self.kernel_manager.list_kernel_ids())
24312433
kernel_msg = trans.ngettext(
24322434
"Shutting down %d kernel", "Shutting down %d kernels", n_kernels
@@ -2453,6 +2455,8 @@ async def cleanup_terminals(self):
24532455

24542456
async def cleanup_extensions(self):
24552457
"""Call shutdown hooks in all extensions."""
2458+
if not getattr(self, "extension_manager", None):
2459+
return
24562460
n_extensions = len(self.extension_manager.extension_apps)
24572461
extension_msg = trans.ngettext(
24582462
"Shutting down %d extension", "Shutting down %d extensions", n_extensions
@@ -2732,6 +2736,8 @@ async def _cleanup(self):
27322736
await self.cleanup_extensions()
27332737
await self.cleanup_kernels()
27342738
await self.cleanup_terminals()
2739+
if getattr(self, "session_manager", None):
2740+
self.session_manager.close()
27352741

27362742
def start_ioloop(self):
27372743
"""Start the IO Loop."""
@@ -2760,7 +2766,8 @@ def start(self):
27602766
async def _stop(self):
27612767
"""Cleanup resources and stop the IO Loop."""
27622768
await self._cleanup()
2763-
self.io_loop.stop()
2769+
if getattr(self, "io_loop", None):
2770+
self.io_loop.stop()
27642771

27652772
def stop(self, from_signal=False):
27662773
"""Cleanup resources and stop the server."""

jupyter_server/services/kernels/kernelmanager.py

+26-12
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def _default_kernel_buffers(self):
149149

150150
def __init__(self, **kwargs):
151151
self.pinned_superclass = MultiKernelManager
152+
self._pending_kernel_tasks = {}
152153
self.pinned_superclass.__init__(self, **kwargs)
153154
self.last_kernel_activity = utcnow()
154155

@@ -216,9 +217,11 @@ async def start_kernel(self, kernel_id=None, path=None, **kwargs):
216217
kwargs["kernel_id"] = kernel_id
217218
kernel_id = await ensure_async(self.pinned_superclass.start_kernel(self, **kwargs))
218219
self._kernel_connections[kernel_id] = 0
219-
fut = asyncio.ensure_future(self._finish_kernel_start(kernel_id))
220+
task = asyncio.create_task(self._finish_kernel_start(kernel_id))
220221
if not getattr(self, "use_pending_kernels", None):
221-
await fut
222+
await task
223+
else:
224+
self._pending_kernel_tasks[kernel_id] = task
222225
# add busy/activity markers:
223226
kernel = self.get_kernel(kernel_id)
224227
kernel.execution_state = "starting"
@@ -245,8 +248,8 @@ async def _finish_kernel_start(self, kernel_id):
245248
if hasattr(km, "ready"):
246249
try:
247250
await km.ready
248-
except Exception:
249-
self.log.exception(km.ready.exception())
251+
except Exception as e:
252+
self.log.exception(e)
250253
return
251254

252255
self._kernel_ports[kernel_id] = km.ports
@@ -372,7 +375,7 @@ def stop_buffering(self, kernel_id):
372375
buffer_info = self._kernel_buffers.pop(kernel_id)
373376
# close buffering streams
374377
for stream in buffer_info["channels"].values():
375-
if not stream.closed():
378+
if not stream.socket.closed:
376379
stream.on_recv(None)
377380
stream.close()
378381

@@ -387,13 +390,18 @@ def stop_buffering(self, kernel_id):
387390
def shutdown_kernel(self, kernel_id, now=False, restart=False):
388391
"""Shutdown a kernel by kernel_id"""
389392
self._check_kernel_id(kernel_id)
390-
self.stop_watching_activity(kernel_id)
391-
self.stop_buffering(kernel_id)
392393

393394
# Decrease the metric of number of kernels
394395
# running for the relevant kernel type by 1
395396
KERNEL_CURRENTLY_RUNNING_TOTAL.labels(type=self._kernels[kernel_id].kernel_name).dec()
396397

398+
if kernel_id in self._pending_kernel_tasks:
399+
task = self._pending_kernel_tasks.pop(kernel_id)
400+
task.cancel()
401+
else:
402+
self.stop_watching_activity(kernel_id)
403+
self.stop_buffering(kernel_id)
404+
397405
self.pinned_superclass.shutdown_kernel(self, kernel_id, now=now, restart=restart)
398406

399407
async def restart_kernel(self, kernel_id, now=False):
@@ -533,7 +541,8 @@ def stop_watching_activity(self, kernel_id):
533541
"""Stop watching IOPub messages on a kernel for activity."""
534542
kernel = self._kernels[kernel_id]
535543
if getattr(kernel, "_activity_stream", None):
536-
kernel._activity_stream.close()
544+
if not kernel._activity_stream.socket.closed:
545+
kernel._activity_stream.close()
537546
kernel._activity_stream = None
538547

539548
def initialize_culler(self):
@@ -638,19 +647,24 @@ def __init__(self, **kwargs):
638647
self.pinned_superclass = AsyncMultiKernelManager
639648
self.pinned_superclass.__init__(self, **kwargs)
640649
self.last_kernel_activity = utcnow()
650+
self._pending_kernel_tasks = {}
641651

642652
async def shutdown_kernel(self, kernel_id, now=False, restart=False):
643653
"""Shutdown a kernel by kernel_id"""
644654
self._check_kernel_id(kernel_id)
645-
self.stop_watching_activity(kernel_id)
646-
self.stop_buffering(kernel_id)
647655

648656
# Decrease the metric of number of kernels
649657
# running for the relevant kernel type by 1
650658
KERNEL_CURRENTLY_RUNNING_TOTAL.labels(type=self._kernels[kernel_id].kernel_name).dec()
651659

660+
if kernel_id in self._pending_kernel_tasks:
661+
task = self._pending_kernel_tasks.pop(kernel_id)
662+
task.cancel()
663+
else:
664+
self.stop_watching_activity(kernel_id)
665+
self.stop_buffering(kernel_id)
666+
652667
# Finish shutting down the kernel before clearing state to avoid a race condition.
653-
ret = await self.pinned_superclass.shutdown_kernel(
668+
return await self.pinned_superclass.shutdown_kernel(
654669
self, kernel_id, now=now, restart=restart
655670
)
656-
return ret

tests/auth/test_authorizer.py

-3
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ async def test_authorized_requests(
205205
send_request,
206206
tmp_path,
207207
jp_serverapp,
208-
jp_cleanup_subprocesses,
209208
method,
210209
url,
211210
body,
@@ -275,5 +274,3 @@ async def test_authorized_requests(
275274

276275
code = await send_request(url, body=body, method=method)
277276
assert code in expected_codes
278-
279-
await jp_cleanup_subprocesses()

tests/services/kernels/test_api.py

+6-16
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async def test_no_kernels(jp_fetch):
6767

6868

6969
@pytest.mark.timeout(TEST_TIMEOUT)
70-
async def test_default_kernels(jp_fetch, jp_base_url, jp_cleanup_subprocesses):
70+
async def test_default_kernels(jp_fetch, jp_base_url):
7171
r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True)
7272
kernel = json.loads(r.body.decode())
7373
assert r.headers["location"] == url_path_join(jp_base_url, "/api/kernels/", kernel["id"])
@@ -79,13 +79,10 @@ async def test_default_kernels(jp_fetch, jp_base_url, jp_cleanup_subprocesses):
7979
["frame-ancestors 'self'", "report-uri " + report_uri, "default-src 'none'"]
8080
)
8181
assert r.headers["Content-Security-Policy"] == expected_csp
82-
await jp_cleanup_subprocesses()
8382

8483

8584
@pytest.mark.timeout(TEST_TIMEOUT)
86-
async def test_main_kernel_handler(
87-
jp_fetch, jp_base_url, jp_cleanup_subprocesses, jp_serverapp, pending_kernel_is_ready
88-
):
85+
async def test_main_kernel_handler(jp_fetch, jp_base_url, jp_serverapp, pending_kernel_is_ready):
8986
# Start the first kernel
9087
r = await jp_fetch(
9188
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
@@ -158,11 +155,10 @@ async def test_main_kernel_handler(
158155
)
159156
kernel3 = json.loads(r.body.decode())
160157
assert isinstance(kernel3, dict)
161-
await jp_cleanup_subprocesses()
162158

163159

164160
@pytest.mark.timeout(TEST_TIMEOUT)
165-
async def test_kernel_handler(jp_fetch, jp_cleanup_subprocesses, pending_kernel_is_ready):
161+
async def test_kernel_handler(jp_fetch, jp_serverapp, pending_kernel_is_ready):
166162
# Create a kernel
167163
r = await jp_fetch(
168164
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
@@ -206,13 +202,10 @@ async def test_kernel_handler(jp_fetch, jp_cleanup_subprocesses, pending_kernel_
206202
with pytest.raises(tornado.httpclient.HTTPClientError) as e:
207203
await jp_fetch("api", "kernels", bad_id, method="DELETE")
208204
assert expected_http_error(e, 404, "Kernel does not exist: " + bad_id)
209-
await jp_cleanup_subprocesses()
210205

211206

212207
@pytest.mark.timeout(TEST_TIMEOUT)
213-
async def test_kernel_handler_startup_error(
214-
jp_fetch, jp_cleanup_subprocesses, jp_serverapp, jp_kernelspecs
215-
):
208+
async def test_kernel_handler_startup_error(jp_fetch, jp_serverapp, jp_kernelspecs):
216209
if getattr(jp_serverapp.kernel_manager, "use_pending_kernels", False):
217210
return
218211

@@ -223,7 +216,7 @@ async def test_kernel_handler_startup_error(
223216

224217
@pytest.mark.timeout(TEST_TIMEOUT)
225218
async def test_kernel_handler_startup_error_pending(
226-
jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp, jp_kernelspecs
219+
jp_fetch, jp_ws_fetch, jp_serverapp, jp_kernelspecs
227220
):
228221
if not getattr(jp_serverapp.kernel_manager, "use_pending_kernels", False):
229222
return
@@ -238,9 +231,7 @@ async def test_kernel_handler_startup_error_pending(
238231

239232

240233
@pytest.mark.timeout(TEST_TIMEOUT)
241-
async def test_connection(
242-
jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header, jp_cleanup_subprocesses
243-
):
234+
async def test_connection(jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
244235
# Create kernel
245236
r = await jp_fetch(
246237
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
@@ -274,4 +265,3 @@ async def test_connection(
274265
r = await jp_fetch("api", "kernels", kid, method="GET")
275266
model = json.loads(r.body.decode())
276267
assert model["connections"] == 0
277-
await jp_cleanup_subprocesses()

tests/services/kernels/test_cull.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
),
4444
],
4545
)
46-
async def test_cull_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses):
46+
async def test_cull_idle(jp_fetch, jp_ws_fetch):
4747
r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True)
4848
kernel = json.loads(r.body.decode())
4949
kid = kernel["id"]
@@ -59,7 +59,6 @@ async def test_cull_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses):
5959
ws.close()
6060
culled = await get_cull_status(kid, jp_fetch) # not connected, should be culled
6161
assert culled
62-
await jp_cleanup_subprocesses()
6362

6463

6564
# Pending kernels was released in Jupyter Client 7.1
@@ -89,9 +88,7 @@ async def test_cull_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses):
8988
],
9089
)
9190
@pytest.mark.timeout(30)
92-
async def test_cull_dead(
93-
jp_fetch, jp_ws_fetch, jp_serverapp, jp_cleanup_subprocesses, jp_kernelspecs
94-
):
91+
async def test_cull_dead(jp_fetch, jp_ws_fetch, jp_serverapp, jp_kernelspecs):
9592
r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True)
9693
kernel = json.loads(r.body.decode())
9794
kid = kernel["id"]
@@ -105,7 +102,6 @@ async def test_cull_dead(
105102
assert model["connections"] == 0
106103
culled = await get_cull_status(kid, jp_fetch) # connected, should not be culled
107104
assert culled
108-
await jp_cleanup_subprocesses()
109105

110106

111107
async def get_cull_status(kid, jp_fetch):

0 commit comments

Comments
 (0)