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,
'last_activity': user.last_activity.isoformat(),
}
if user.spawn_pending:
if user.spawners['']._spawn_pending:
model['pending'] = 'spawn'
elif user.stop_pending:
elif user.spawners['']._stop_pending:
model['pending'] = 'stop'
return model

View File

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

View File

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

View File

@@ -45,6 +45,11 @@ class Spawner(LoggingConfigurable):
is created for each user. If there are 20 JupyterHub users, there will be 20
instances of the subclass.
"""
# private attributes for tracking status
_spawn_pending = False
_stop_pending = False
_waiting_for_response = False
db = Any()
user = Any()
@@ -500,6 +505,8 @@ class Spawner(LoggingConfigurable):
Doesn't expect shell expansion to happen.
"""
args = []
if 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)
assert app_user.spawner is not None
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)
assert status is None
@@ -458,38 +458,38 @@ def test_slow_spawn(app, io_loop, no_patience, request):
assert r.status_code == 202
app_user = get_app_user(app, name)
assert app_user.spawner is not None
assert app_user.spawn_pending
assert not app_user.stop_pending
assert app_user.spawner._spawn_pending
assert not app_user.spawner._stop_pending
@gen.coroutine
def wait_spawn():
while app_user.spawn_pending:
while app_user.spawner._spawn_pending:
yield gen.sleep(0.1)
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)
assert status is None
@gen.coroutine
def wait_stop():
while app_user.stop_pending:
while app_user.pawner._stop_pending:
yield gen.sleep(0.1)
r = api_request(app, 'users', name, 'server', method='delete')
r.raise_for_status()
assert r.status_code == 202
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.raise_for_status()
assert r.status_code == 202
assert app_user.spawner is not None
assert app_user.stop_pending
assert app_user.spawner._stop_pending
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
r = api_request(app, 'users', name, 'server', method='delete')
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')
app_user = get_app_user(app, name)
assert app_user.spawner is not None
assert app_user.spawn_pending
assert app_user.spawner._spawn_pending
@gen.coroutine
def wait_pending():
while app_user.spawn_pending:
while app_user.spawner._spawn_pending:
yield gen.sleep(0.1)
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)
assert status is not None

View File

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