Merge pull request #4844 from minrk/allow-stop-during-start

allow stop while start is pending
This commit is contained in:
Min RK
2024-07-01 14:36:36 +02:00
committed by GitHub
3 changed files with 33 additions and 20 deletions

View File

@@ -694,14 +694,22 @@ class UserServerAPIHandler(APIHandler):
asyncio.ensure_future(_remove_spawner(spawner._stop_future)) asyncio.ensure_future(_remove_spawner(spawner._stop_future))
return return
if spawner.pending:
raise web.HTTPError(
400,
f"{spawner._log_name} is pending {spawner.pending}, please wait",
)
stop_future = None stop_future = None
if spawner.ready: if spawner.pending:
# we are interrupting a pending start
# hopefully nothing gets leftover
self.log.warning(
f"Interrupting spawner {spawner._log_name}, pending {spawner.pending}"
)
spawn_future = spawner._spawn_future
if spawn_future:
spawn_future.cancel()
# Give cancel a chance to resolve?
# not sure what we would wait for here,
await asyncio.sleep(1)
stop_future = await self.stop_single_user(user, server_name)
elif spawner.ready:
# include notify, so that a server that died is noticed immediately # include notify, so that a server that died is noticed immediately
status = await spawner.poll_and_notify() status = await spawner.poll_and_notify()
if status is None: if status is None:
@@ -837,7 +845,9 @@ class SpawnProgressAPIHandler(APIHandler):
# not pending, no progress to fetch # not pending, no progress to fetch
# check if spawner has just failed # check if spawner has just failed
f = spawn_future f = spawn_future
if f and f.done() and f.exception(): if f and f.cancelled():
failed_event['message'] = "Spawn cancelled"
elif f and f.done() and f.exception():
exc = f.exception() exc = f.exception()
message = getattr(exc, "jupyterhub_message", str(exc)) message = getattr(exc, "jupyterhub_message", str(exc))
failed_event['message'] = f"Spawn failed: {message}" failed_event['message'] = f"Spawn failed: {message}"
@@ -876,7 +886,9 @@ class SpawnProgressAPIHandler(APIHandler):
else: else:
# what happened? Maybe spawn failed? # what happened? Maybe spawn failed?
f = spawn_future f = spawn_future
if f and f.done() and f.exception(): if f and f.cancelled():
failed_event['message'] = "Spawn cancelled"
elif f and f.done() and f.exception():
exc = f.exception() exc = f.exception()
message = getattr(exc, "jupyterhub_message", str(exc)) message = getattr(exc, "jupyterhub_message", str(exc))
failed_event['message'] = f"Spawn failed: {message}" failed_event['message'] = f"Spawn failed: {message}"

View File

@@ -1558,23 +1558,20 @@ async def test_start_stop_race(app, no_patience, slow_spawn):
r = await api_request(app, 'users', user.name, 'server', method='post') r = await api_request(app, 'users', user.name, 'server', method='post')
assert r.status_code == 202 assert r.status_code == 202
assert spawner.pending == 'spawn' assert spawner.pending == 'spawn'
spawn_future = spawner._spawn_future
# additional spawns while spawning shouldn't trigger a new spawn # additional spawns while spawning shouldn't trigger a new spawn
with mock.patch.object(spawner, 'start') as m: with mock.patch.object(spawner, 'start') as m:
r = await api_request(app, 'users', user.name, 'server', method='post') r = await api_request(app, 'users', user.name, 'server', method='post')
assert r.status_code == 202 assert r.status_code == 202
assert m.call_count == 0 assert m.call_count == 0
# stop while spawning is not okay # stop while spawning is okay now
r = await api_request(app, 'users', user.name, 'server', method='delete')
assert r.status_code == 400
while not spawner.ready:
await asyncio.sleep(0.1)
spawner.delay = 3 spawner.delay = 3
# stop the spawner
r = await api_request(app, 'users', user.name, 'server', method='delete') r = await api_request(app, 'users', user.name, 'server', method='delete')
assert r.status_code == 202 assert r.status_code == 202
assert spawner.pending == 'stop' assert spawner.pending == 'stop'
assert spawn_future.cancelled()
assert spawner._spawn_future is None
# make sure we get past deleting from the proxy # make sure we get past deleting from the proxy
await asyncio.sleep(1) await asyncio.sleep(1)
# additional stops while stopping shouldn't trigger a new stop # additional stops while stopping shouldn't trigger a new stop

View File

@@ -1,13 +1,13 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import asyncio
import json import json
import warnings import warnings
from collections import defaultdict from collections import defaultdict
from datetime import timedelta
from urllib.parse import quote, urlparse, urlunparse from urllib.parse import quote, urlparse, urlunparse
from sqlalchemy import inspect from sqlalchemy import inspect
from tornado import gen, web from tornado import web
from tornado.httputil import urlencode from tornado.httputil import urlencode
from tornado.log import app_log from tornado.log import app_log
@@ -912,9 +912,13 @@ class User:
spawner.cert_paths = await maybe_future(spawner.move_certs(hub_paths)) spawner.cert_paths = await maybe_future(spawner.move_certs(hub_paths))
self.log.debug("Calling Spawner.start for %s", spawner._log_name) self.log.debug("Calling Spawner.start for %s", spawner._log_name)
f = maybe_future(spawner.start()) f = maybe_future(spawner.start())
# commit any changes in spawner.start (always commit db changes before yield) # commit any changes in spawner.start (always commit db changes before await)
db.commit() db.commit()
url = await gen.with_timeout(timedelta(seconds=spawner.start_timeout), f) # gen.with_timeout protects waited-for tasks from cancellation,
# whereas wait_for cancels tasks that don't finish within timeout.
# we want this task to halt if it doesn't return in the time limit.
await asyncio.wait_for(f, timeout=spawner.start_timeout)
url = f.result()
if url: if url:
# get ip, port info from return value of start() # get ip, port info from return value of start()
if isinstance(url, str): if isinstance(url, str):