From 8e3553462c53cfa2f042a8a37725e9c234f1b96e Mon Sep 17 00:00:00 2001 From: Min RK Date: Sat, 26 Aug 2017 11:57:05 -0400 Subject: [PATCH] exercise start/stop race conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this doesn’t cover all the edge cases of each possible stage for the races, but it gets the basics covered. --- jupyterhub/apihandlers/users.py | 5 +++- jupyterhub/handlers/base.py | 2 +- jupyterhub/tests/test_api.py | 44 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) 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')