move some state to Spawner

now that there are more than one per user
This commit is contained in:
Min RK
2017-06-21 16:33:16 +02:00
parent e33e34748f
commit 5da4348c2d
7 changed files with 103 additions and 107 deletions

View File

@@ -97,9 +97,9 @@ class APIHandler(BaseHandler):
'pending': None, 'pending': None,
'last_activity': user.last_activity.isoformat(), 'last_activity': user.last_activity.isoformat(),
} }
if user.spawn_pending: if user.spawners['']._spawn_pending:
model['pending'] = 'spawn' model['pending'] = 'spawn'
elif user.stop_pending: elif user.spawners['']._stop_pending:
model['pending'] = 'stop' model['pending'] = 'stop'
return model return model

View File

@@ -146,11 +146,11 @@ class UserAPIHandler(APIHandler):
raise web.HTTPError(404) raise web.HTTPError(404)
if user.name == self.get_current_user().name: if user.name == self.get_current_user().name:
raise web.HTTPError(400, "Cannot delete yourself!") raise web.HTTPError(400, "Cannot delete yourself!")
if user.stop_pending: if user.spawner._stop_pending:
raise web.HTTPError(400, "%s's server is in the process of stopping, please wait." % name) raise web.HTTPError(400, "%s's server is in the process of stopping, please wait." % name)
if user.running: if user.running:
yield self.stop_single_user(user) yield self.stop_single_user(user)
if user.stop_pending: if user.spawner._stop_pending:
raise web.HTTPError(400, "%s's server is in the process of stopping, please wait." % name) raise web.HTTPError(400, "%s's server is in the process of stopping, please wait." % name)
yield gen.maybe_future(self.authenticator.delete_user(user)) yield gen.maybe_future(self.authenticator.delete_user(user))
@@ -193,14 +193,14 @@ class UserServerAPIHandler(APIHandler):
options = self.get_json_body() options = self.get_json_body()
yield self.spawn_single_user(user, options=options) yield self.spawn_single_user(user, options=options)
status = 202 if user.spawn_pending else 201 status = 202 if user.spawner._spawn_pending else 201
self.set_status(status) self.set_status(status)
@gen.coroutine @gen.coroutine
@admin_or_self @admin_or_self
def delete(self, name): def delete(self, name):
user = self.find_user(name) user = self.find_user(name)
if user.stop_pending: if user.spawner._stop_pending:
self.set_status(202) self.set_status(202)
return return
if not user.running: if not user.running:
@@ -210,7 +210,7 @@ class UserServerAPIHandler(APIHandler):
if status is not None: if status is not None:
raise web.HTTPError(400, "%s's server is not running" % name) raise web.HTTPError(400, "%s's server is not running" % name)
yield self.stop_single_user(user) yield self.stop_single_user(user)
status = 202 if user.stop_pending else 204 status = 202 if user.spawner._stop_pending else 204
self.set_status(status) self.set_status(status)
@@ -221,10 +221,11 @@ class UserCreateNamedServerAPIHandler(APIHandler):
""" """
@gen.coroutine @gen.coroutine
@admin_or_self @admin_or_self
def post(self, name): def post(self, name, server_name=''):
user = self.find_user(name) user = self.find_user(name)
if user is None: if user is None:
raise web.HTTPError(404, "No such user %r" % name) raise web.HTTPError(404, "No such user %r" % name)
#if user.running: #if user.running:
# # include notify, so that a server that died is noticed immediately # # include notify, so that a server that died is noticed immediately
# state = yield user.spawner.poll_and_notify() # state = yield user.spawner.poll_and_notify()
@@ -232,8 +233,8 @@ class UserCreateNamedServerAPIHandler(APIHandler):
# raise web.HTTPError(400, "%s's server is already running" % name) # raise web.HTTPError(400, "%s's server is already running" % name)
options = self.get_json_body() options = self.get_json_body()
yield self.spawn_single_user(user, options=options) yield self.spawn_single_user(user, server_name, options=options)
status = 202 if user.spawn_pending else 201 status = 202 if user.spawner._spawn_pending else 201
self.set_status(status) self.set_status(status)
@@ -248,17 +249,18 @@ class UserDeleteNamedServerAPIHandler(APIHandler):
@admin_or_self @admin_or_self
def delete(self, name, server_name): def delete(self, name, server_name):
user = self.find_user(name) user = self.find_user(name)
if user.stop_pending: spawner = user.spawners[server_name]
if spawner._stop_pending:
self.set_status(202) self.set_status(202)
return return
#if not user.running: #if not user.running:
# raise web.HTTPError(400, "%s's server is not running" % name) # raise web.HTTPError(400, "%s's server is not running" % name)
# include notify, so that a server that died is noticed immediately # include notify, so that a server that died is noticed immediately
status = yield user.spawner.poll_and_notify() status = yield spawner.poll_and_notify()
if status is not None: if status is not None:
raise web.HTTPError(400, "%s's server is not running" % name) raise web.HTTPError(400, "%s's server is not running" % name)
yield self.stop_single_user(user) yield self.stop_single_user(user, server_name)
status = 202 if user.stop_pending else 204 status = 202 if spawner._stop_pending else 204
self.set_status(status) self.set_status(status)
class UserAdminAccessAPIHandler(APIHandler): class UserAdminAccessAPIHandler(APIHandler):
@@ -288,7 +290,7 @@ default_handlers = [
(r"/api/users", UserListAPIHandler), (r"/api/users", UserListAPIHandler),
(r"/api/users/([^/]+)", UserAPIHandler), (r"/api/users/([^/]+)", UserAPIHandler),
(r"/api/users/([^/]+)/server", UserServerAPIHandler), (r"/api/users/([^/]+)/server", UserServerAPIHandler),
(r"/api/users/([^/]+)/servers", UserCreateNamedServerAPIHandler), (r"/api/users/([^/]+)/servers/([^/]*)", UserCreateNamedServerAPIHandler),
(r"/api/users/([^/]+)/servers/([^/]+)", UserDeleteNamedServerAPIHandler), (r"/api/users/([^/]+)/servers/([^/]*)", UserDeleteNamedServerAPIHandler),
(r"/api/users/([^/]+)/admin-access", UserAdminAccessAPIHandler), (r"/api/users/([^/]+)/admin-access", UserAdminAccessAPIHandler),
] ]

View File

@@ -319,12 +319,13 @@ class BaseHandler(RequestHandler):
return self.settings.get('spawner_class', LocalProcessSpawner) return self.settings.get('spawner_class', LocalProcessSpawner)
@gen.coroutine @gen.coroutine
def spawn_single_user(self, user, options=None): def spawn_single_user(self, user, server_name='', options=None):
if user.spawn_pending: if server_name in user.spawners and user.spawners[server_name]._spawn_pending:
raise RuntimeError("Spawn already pending for: %s" % user.name) raise RuntimeError("Spawn already pending for: %s" % user.name)
tic = IOLoop.current().time() tic = IOLoop.current().time()
f = user.spawn(options) f = user.spawn(server_name, options)
spawner = user.spawners[server_name]
@gen.coroutine @gen.coroutine
def finish_user_spawn(f=None): def finish_user_spawn(f=None):
@@ -340,14 +341,14 @@ class BaseHandler(RequestHandler):
self.log.info("User %s server took %.3f seconds to start", user.name, toc-tic) self.log.info("User %s server took %.3f seconds to start", user.name, toc-tic)
self.statsd.timing('spawner.success', (toc - tic) * 1000) self.statsd.timing('spawner.success', (toc - tic) * 1000)
yield self.proxy.add_user(user) yield self.proxy.add_user(user)
user.spawner.add_poll_callback(self.user_stopped, user) spawner.add_poll_callback(self.user_stopped, user)
try: try:
yield gen.with_timeout(timedelta(seconds=self.slow_spawn_timeout), f) yield gen.with_timeout(timedelta(seconds=self.slow_spawn_timeout), f)
except gen.TimeoutError: except gen.TimeoutError:
# waiting_for_response indicates server process has started, # waiting_for_response indicates server process has started,
# but is yet to become responsive. # but is yet to become responsive.
if not user.waiting_for_response: if not spawner._waiting_for_response:
# still in Spawner.start, which is taking a long time # still in Spawner.start, which is taking a long time
# we shouldn't poll while spawn is incomplete. # we shouldn't poll while spawn is incomplete.
self.log.warning("User %s's server is slow to start (timeout=%s)", self.log.warning("User %s's server is slow to start (timeout=%s)",
@@ -387,7 +388,7 @@ class BaseHandler(RequestHandler):
@gen.coroutine @gen.coroutine
def stop_single_user(self, user): def stop_single_user(self, user):
if user.stop_pending: if user.spawner._stop_pending:
raise RuntimeError("Stop already pending for: %s" % user.name) raise RuntimeError("Stop already pending for: %s" % user.name)
tic = IOLoop.current().time() tic = IOLoop.current().time()
yield self.proxy.delete_user(user) yield self.proxy.delete_user(user)
@@ -408,7 +409,7 @@ class BaseHandler(RequestHandler):
try: try:
yield gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), f) yield gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), f)
except gen.TimeoutError: except gen.TimeoutError:
if user.stop_pending: if user.spawner._stop_pending:
# hit timeout, but stop is still pending # hit timeout, but stop is still pending
self.log.warning("User %s server is slow to stop", user.name) self.log.warning("User %s server is slow to stop", user.name)
# schedule finish for when the server finishes stopping # schedule finish for when the server finishes stopping
@@ -535,23 +536,23 @@ class UserSpawnHandler(BaseHandler):
""", self.request.full_url(), self.proxy.public_url) """, self.request.full_url(), self.proxy.public_url)
# logged in as correct user, spawn the server # logged in as correct user, spawn the server
if current_user.spawner: spawner = current_user.spawner
if current_user.spawn_pending: if spawner._spawn_pending:
# spawn has started, but not finished # spawn has started, but not finished
self.statsd.incr('redirects.user_spawn_pending', 1) self.statsd.incr('redirects.user_spawn_pending', 1)
html = self.render_template("spawn_pending.html", user=current_user) html = self.render_template("spawn_pending.html", user=current_user)
self.finish(html) self.finish(html)
return return
# spawn has supposedly finished, check on the status # spawn has supposedly finished, check on the status
status = yield current_user.spawner.poll() status = yield spawner.poll()
if status is not None: if status is not None:
if current_user.spawner.options_form: if spawner.options_form:
self.redirect(url_concat(url_path_join(self.hub.base_url, 'spawn'), self.redirect(url_concat(url_path_join(self.hub.base_url, 'spawn'),
{'next': self.request.uri})) {'next': self.request.uri}))
return return
else: else:
yield self.spawn_single_user(current_user) yield self.spawn_single_user(current_user)
# set login cookie anew # set login cookie anew
self.set_login_cookie(current_user) self.set_login_cookie(current_user)
without_prefix = self.request.uri[len(self.hub.base_url):] without_prefix = self.request.uri[len(self.hub.base_url):]

View File

@@ -140,7 +140,7 @@ class Proxy(LoggingConfigurable):
user.name, user.proxy_path, user.server.host, user.name, user.proxy_path, user.server.host,
) )
if user.spawn_pending: if user.spawner._spawn_pending:
raise RuntimeError( raise RuntimeError(
"User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name) "User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name)

View File

@@ -46,6 +46,11 @@ class Spawner(LoggingConfigurable):
instances of the subclass. instances of the subclass.
""" """
# private attributes for tracking status
_spawn_pending = False
_stop_pending = False
_waiting_for_response = False
db = Any() db = Any()
user = Any() user = Any()
hub = Any() hub = Any()
@@ -500,6 +505,8 @@ class Spawner(LoggingConfigurable):
Doesn't expect shell expansion to happen. Doesn't expect shell expansion to happen.
""" """
args = []
if self.ip: if self.ip:
args.append('--ip="%s"' % self.ip) args.append('--ip="%s"' % self.ip)

View File

@@ -409,7 +409,7 @@ def test_spawn(app, io_loop):
app_user = get_app_user(app, name) app_user = get_app_user(app, name)
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.spawner.user_options == options assert app_user.spawner.user_options == options
assert not app_user.spawn_pending assert not app_user.spawner._spawn_pending
status = io_loop.run_sync(app_user.spawner.poll) status = io_loop.run_sync(app_user.spawner.poll)
assert status is None assert status is None
@@ -458,38 +458,38 @@ def test_slow_spawn(app, io_loop, no_patience, request):
assert r.status_code == 202 assert r.status_code == 202
app_user = get_app_user(app, name) app_user = get_app_user(app, name)
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.spawn_pending assert app_user.spawner._spawn_pending
assert not app_user.stop_pending assert not app_user.spawner._stop_pending
@gen.coroutine @gen.coroutine
def wait_spawn(): def wait_spawn():
while app_user.spawn_pending: while app_user.spawner._spawn_pending:
yield gen.sleep(0.1) yield gen.sleep(0.1)
io_loop.run_sync(wait_spawn) io_loop.run_sync(wait_spawn)
assert not app_user.spawn_pending assert not app_user.spawner._spawn_pending
status = io_loop.run_sync(app_user.spawner.poll) status = io_loop.run_sync(app_user.spawner.poll)
assert status is None assert status is None
@gen.coroutine @gen.coroutine
def wait_stop(): def wait_stop():
while app_user.stop_pending: while app_user.pawner._stop_pending:
yield gen.sleep(0.1) yield gen.sleep(0.1)
r = api_request(app, 'users', name, 'server', method='delete') r = api_request(app, 'users', name, 'server', method='delete')
r.raise_for_status() r.raise_for_status()
assert r.status_code == 202 assert r.status_code == 202
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.stop_pending assert app_user.spawner._stop_pending
r = api_request(app, 'users', name, 'server', method='delete') r = api_request(app, 'users', name, 'server', method='delete')
r.raise_for_status() r.raise_for_status()
assert r.status_code == 202 assert r.status_code == 202
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.stop_pending assert app_user.spawner._stop_pending
io_loop.run_sync(wait_stop) io_loop.run_sync(wait_stop)
assert not app_user.stop_pending assert not app_user.spawner._stop_pending
assert app_user.spawner is not None assert app_user.spawner is not None
r = api_request(app, 'users', name, 'server', method='delete') r = api_request(app, 'users', name, 'server', method='delete')
assert r.status_code == 400 assert r.status_code == 400
@@ -506,15 +506,15 @@ def test_never_spawn(app, io_loop, no_patience, request):
r = api_request(app, 'users', name, 'server', method='post') r = api_request(app, 'users', name, 'server', method='post')
app_user = get_app_user(app, name) app_user = get_app_user(app, name)
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.spawn_pending assert app_user.spawner._spawn_pending
@gen.coroutine @gen.coroutine
def wait_pending(): def wait_pending():
while app_user.spawn_pending: while app_user.spawner._spawn_pending:
yield gen.sleep(0.1) yield gen.sleep(0.1)
io_loop.run_sync(wait_pending) io_loop.run_sync(wait_pending)
assert not app_user.spawn_pending assert not app_user.spawner._spawn_pending
status = io_loop.run_sync(app_user.spawner.poll) status = io_loop.run_sync(app_user.spawner.poll)
assert status is not None assert status is not None

View File

@@ -1,6 +1,7 @@
# 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.
from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from urllib.parse import quote, urlparse from urllib.parse import quote, urlparse
@@ -96,13 +97,10 @@ class User(HasTraits):
if self.orm_user: if self.orm_user:
id = self.orm_user.id id = self.orm_user.id
self.orm_user = change['new'].query(orm.User).filter(orm.User.id == id).first() self.orm_user = change['new'].query(orm.User).filter(orm.User.id == id).first()
self.spawner.db = self.db for spawner in self.spawners.values():
spawner.db = self.db
orm_user = None orm_user = None
spawner = None
spawn_pending = False
stop_pending = False
waiting_for_response = False
@property @property
def authenticator(self): def authenticator(self):
@@ -112,7 +110,6 @@ class User(HasTraits):
def spawner_class(self): def spawner_class(self):
return self.settings.get('spawner_class', LocalProcessSpawner) return self.settings.get('spawner_class', LocalProcessSpawner)
def __init__(self, orm_user, settings=None, **kwargs): def __init__(self, orm_user, settings=None, **kwargs):
self.orm_user = orm_user self.orm_user = orm_user
self.settings = settings or {} self.settings = settings or {}
@@ -124,7 +121,11 @@ class User(HasTraits):
self.base_url = url_path_join( self.base_url = url_path_join(
self.settings.get('base_url', '/'), 'user', self.escaped_name) self.settings.get('base_url', '/'), 'user', self.escaped_name)
self.spawner = self.spawner_class( self.spawners = defaultdict(self._new_spawner)
def _new_spawner(self):
"""Create a new spawner"""
return self.spawner_class(
user=self, user=self,
db=self.db, db=self.db,
hub=self.settings.get('hub'), hub=self.settings.get('hub'),
@@ -132,19 +133,14 @@ class User(HasTraits):
config=self.settings.get('config'), config=self.settings.get('config'),
) )
# singleton property, self.spawner maps onto spawner with empty server_name
@property @property
def get_spawners(self): def spawner(self):
return self._instances return self.spawners['']
@property @spawner.setter
def get_spawner(self, server_name): def spawner(self, spawner):
try: self.spawners[''] = spawner
return self._instances[server_name]
except KeyError as err:
self.log.warning("spawner for server named %s doesn't exist" % server_name)
def save_spawner(self, server_name):
self._instances[server_name] = self.spawner
# pass get/setattr to ORM user # pass get/setattr to ORM user
@@ -163,12 +159,13 @@ class User(HasTraits):
def __repr__(self): def __repr__(self):
return repr(self.orm_user) return repr(self.orm_user)
@property # FIX-ME CHECK IF STILL NEEDED @property
def running(self): def running(self):
"""property for whether a user has a running server""" """property for whether a user has a running server"""
if self.spawn_pending or self.stop_pending: spawner = self.spawner
if spawner._spawn_pending or spawner._stop_pending:
return False # server is not running if spawn or stop is still pending return False # server is not running if spawn or stop is still pending
if self.server is None: if spawner.server is None:
return False return False
return True return True
@@ -220,7 +217,7 @@ class User(HasTraits):
return self.base_url return self.base_url
@gen.coroutine @gen.coroutine
def spawn(self, options=None): def spawn(self, server_name='', options=None):
"""Start the user's spawner """Start the user's spawner
depending from the value of JupyterHub.allow_named_servers depending from the value of JupyterHub.allow_named_servers
@@ -234,15 +231,10 @@ class User(HasTraits):
url of the server will be /user/:name/:server_name url of the server will be /user/:name/:server_name
""" """
db = self.db db = self.db
if self.allow_named_servers: if self.allow_named_servers and not server_name:
if options is not None and 'server_name' in options: server_name = default_server_name(self)
server_name = options['server_name']
else: base_url = url_path_join(self.base_url, server_name)
server_name = default_server_name(self)
base_url = url_path_join(self.base_url, server_name)
else:
server_name = ''
base_url = self.base_url
orm_server = orm.Server( orm_server = orm.Server(
name=server_name, name=server_name,
@@ -255,14 +247,10 @@ class User(HasTraits):
server = Server(orm_server=orm_server) server = Server(orm_server=orm_server)
spawner = self.spawner spawner = self.spawners[server_name]
# Save spawner's instance inside self._instances
self.save_spawner(server_name)
# Passing server, server_name and options to the spawner # Passing server, server_name and options to the spawner
spawner.server = server spawner.server = server
spawner.server_name = server_name
spawner.user_options = options or {} spawner.user_options = options or {}
# we are starting a new server, make sure it doesn't restore state # we are starting a new server, make sure it doesn't restore state
spawner.clear_state() spawner.clear_state()
@@ -294,7 +282,7 @@ class User(HasTraits):
if (authenticator): if (authenticator):
yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner)) yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner))
self.spawn_pending = True spawner._spawn_pending = True
# wait for spawner.start to return # wait for spawner.start to return
try: try:
f = spawner.start() f = spawner.start()
@@ -336,10 +324,10 @@ class User(HasTraits):
spawner.start_polling() spawner.start_polling()
# store state # store state
self.state = spawner.get_state() self.state[server_name] = spawner.get_state()
self.last_activity = datetime.utcnow() self.last_activity = datetime.utcnow()
db.commit() db.commit()
self.waiting_for_response = True spawner._waiting_for_response = True
try: try:
yield server.wait_up(http=True, timeout=spawner.http_timeout) yield server.wait_up(http=True, timeout=spawner.http_timeout)
except Exception as e: except Exception as e:
@@ -367,32 +355,30 @@ class User(HasTraits):
# raise original TimeoutError # raise original TimeoutError
raise e raise e
finally: finally:
self.waiting_for_response = False spawner._waiting_for_response = False
self.spawn_pending = False spawner._spawn_pending = False
return self return self
@gen.coroutine @gen.coroutine
def stop(self): def stop(self, server_name=''):
"""Stop the user's spawner """Stop the user's spawner
and cleanup after it. and cleanup after it.
""" """
self.spawn_pending = False spawner = self.spawners[server_name]
spawner = self.spawner spawner._spawn_pending = False
self.spawner.stop_polling() spawner.stop_polling()
self.stop_pending = True spawner._stop_pending = True
try: try:
api_token = self.spawner.api_token api_token = spawner.api_token
status = yield spawner.poll() status = yield spawner.poll()
if status is None: if status is None:
yield self.spawner.stop() yield spawner.stop()
spawner.clear_state() spawner.clear_state()
self.state = spawner.get_state() self.state = spawner.get_state()
self.last_activity = datetime.utcnow() self.last_activity = datetime.utcnow()
# Cleanup defunct servers: delete entry and API token for each server # remove server entry from db
for server in self.servers: self.db.delete(spawner.server.orm_server)
# remove server entry from db
self.db.delete(server)
if not spawner.will_resume: if not spawner.will_resume:
# find and remove the API token if the spawner isn't # find and remove the API token if the spawner isn't
# going to re-use it next time # going to re-use it next time
@@ -401,7 +387,7 @@ class User(HasTraits):
self.db.delete(orm_token) self.db.delete(orm_token)
self.db.commit() self.db.commit()
finally: finally:
self.stop_pending = False spawner._stop_pending = False
# trigger post-spawner hook on authenticator # trigger post-spawner hook on authenticator
auth = spawner.authenticator auth = spawner.authenticator
if auth: if auth: