mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 21:43:01 +00:00
move some state to Spawner
now that there are more than one per user
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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),
|
||||
]
|
||||
|
@@ -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):]
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user