diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 63b9e995..01682d1a 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -231,7 +231,10 @@ class UserServerAPIHandler(APIHandler): return if not spawner.ready: - raise web.HTTPError(400, "%s is not running" % spawner._log_name) + raise web.HTTPError( + 400, "%s is not running %s" % + (spawner._log_name, '(pending: %s)' % spawner.pending if spawner.pending else '') + ) # include notify, so that a server that died is noticed immediately status = yield spawner.poll_and_notify() if status is not None: diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 6d0ba398..f11ccb16 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -514,7 +514,7 @@ class BaseHandler(RequestHandler): raise KeyError("User %s has no such spawner %r", user.name, name) spawner = user.spawners[name] if spawner.pending: - raise RuntimeError("%s pending %s" % (user_server_name, spawner.pending)) + raise RuntimeError("%s pending %s" % (spawner._log_name, spawner.pending)) # set user._stop_pending before doing anything async # to avoid races spawner._stop_pending = True diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 52ede1ed..555ae8f1 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -654,6 +654,50 @@ def test_active_server_limit(app, request): assert counts['pending'] == 0 +@mark.gen_test +def test_start_stop_race(app, no_patience, slow_spawn): + user = add_user(app.db, app, name='panda') + spawner = user.spawner + # start the server + r = yield api_request(app, 'users', user.name, 'server', method='post') + assert r.status_code == 202 + assert spawner.pending == 'spawn' + # additional spawns while spawning shouldn't trigger a new spawn + with mock.patch.object(spawner, 'start') as m: + r = yield api_request(app, 'users', user.name, 'server', method='post') + assert r.status_code == 202 + assert m.call_count == 0 + + # stop while spawning is not okay + r = yield api_request(app, 'users', user.name, 'server', method='delete') + assert r.status_code == 400 + while not spawner.ready: + yield gen.sleep(0.1) + + spawner.delay = 3 + # stop the spawner + r = yield api_request(app, 'users', user.name, 'server', method='delete') + assert r.status_code == 202 + assert spawner.pending == 'stop' + # make sure we get past deleting from the proxy + yield gen.sleep(1) + # additional stops while stopping shouldn't trigger a new stop + with mock.patch.object(spawner, 'stop') as m: + r = yield api_request(app, 'users', user.name, 'server', method='delete') + assert r.status_code == 202 + assert m.call_count == 0 + # start while stopping is not allowed + with mock.patch.object(spawner, 'start') as m: + r = yield api_request(app, 'users', user.name, 'server', method='post') + assert r.status_code == 400 + + while spawner.active: + yield gen.sleep(0.1) + # start after stop is okay + r = yield api_request(app, 'users', user.name, 'server', method='post') + assert r.status_code == 202 + + @mark.gen_test def test_get_proxy(app): r = yield api_request(app, 'proxy')