remove Spawner.fromJSON

load state on `__init__` instead

Makes more sense now that state can persist
across server instances (e.g. docker container_id)
This commit is contained in:
MinRK
2014-10-10 12:28:27 -07:00
parent d8ef6d59c1
commit a8548164cd
3 changed files with 64 additions and 36 deletions

View File

@@ -448,20 +448,25 @@ class JupyterHubApp(Application):
for user in db.query(orm.User): for user in db.query(orm.User):
if not user.state: if not user.state:
# without spawner state, server isn't valid
user.server = None
user_summaries.append(_user_summary(user)) user_summaries.append(_user_summary(user))
continue continue
self.log.debug("Loading state for %s from db", user.name) self.log.debug("Loading state for %s from db", user.name)
spawner = self.spawner_class.fromJSON(user.state, user=user, hub=self.hub, config=self.config) user.spawner = spawner = self.spawner_class(
user=user, hub=self.hub, config=self.config,
)
status = run_sync(spawner.poll) status = run_sync(spawner.poll)
if status is None: if status is None:
self.log.info("User %s still running", user.name) self.log.info("%s still running", user.name)
user.spawner = spawner
spawner.add_poll_callback(user_stopped, user) spawner.add_poll_callback(user_stopped, user)
spawner.start_polling() spawner.start_polling()
else: else:
self.log.warn("Failed to load state for %s, assuming server is not running.", user.name) # user not running. This is expected if server is None,
# not running, state is invalid # but indicates the user's server died while the Hub wasn't running
user.state = {} # if user.server is defined.
log = self.log.warn if user.server else self.log.debug
log("%s not running.", user.name)
user.server = None user.server = None
user_summaries.append(_user_summary(user)) user_summaries.append(_user_summary(user))
@@ -508,7 +513,8 @@ class JupyterHubApp(Application):
'--api-port', str(self.proxy.api_server.port), '--api-port', str(self.proxy.api_server.port),
'--default-target', self.hub.server.host, '--default-target', self.hub.server.host,
] ]
if self.log_level == logging.DEBUG: if False:
# if self.log_level == logging.DEBUG:
cmd.extend(['--log-level', 'debug']) cmd.extend(['--log-level', 'debug'])
if self.ssl_key: if self.ssl_key:
cmd.extend(['--ssl-key', self.ssl_key]) cmd.extend(['--ssl-key', self.ssl_key])

View File

@@ -305,13 +305,17 @@ class User(Base):
api_token = self.new_api_token() api_token = self.new_api_token()
db.add(api_token) db.add(api_token)
db.commit() db.commit()
spawner = self.spawner = spawner_class( spawner = self.spawner = spawner_class(
config=config, config=config,
user=self, user=self,
hub=hub, hub=hub,
api_token=api_token.token, api_token=api_token.token,
) )
# we are starting a new server, make sure it doesn't restore state
spawner.clear_state()
yield spawner.start() yield spawner.start()
spawner.start_polling() spawner.start_polling()
@@ -335,6 +339,7 @@ class User(Base):
status = yield self.spawner.poll() status = yield self.spawner.poll()
if status is None: if status is None:
yield self.spawner.stop() yield self.spawner.stop()
self.spawner.clear_state()
self.state = self.spawner.get_state() self.state = self.spawner.get_state()
self.last_activity = datetime.utcnow() self.last_activity = datetime.utcnow()
self.server = None self.server = None

View File

@@ -80,15 +80,10 @@ class Spawner(LoggingConfigurable):
help="""The command used for starting notebooks.""" help="""The command used for starting notebooks."""
) )
@classmethod def __init__(self, **kwargs):
def fromJSON(cls, state, **kwargs): super(Spawner, self).__init__(**kwargs)
"""Create a new instance, and load its JSON state if self.user.state:
self.load_state(self.user.state)
state will be a dict, loaded from JSON in the database.
"""
inst = cls(**kwargs)
inst.load_state(state)
return inst
def load_state(self, state): def load_state(self, state):
"""load state from the database """load state from the database
@@ -101,9 +96,10 @@ class Spawner(LoggingConfigurable):
See Also See Also
-------- --------
get_state get_state, clear_state
""" """
pass if 'api_token' in state:
self.api_token = state['api_token']
def get_state(self): def get_state(self):
"""store the state necessary for load_state """store the state necessary for load_state
@@ -117,7 +113,19 @@ class Spawner(LoggingConfigurable):
state: dict state: dict
a JSONable dict of state a JSONable dict of state
""" """
return dict(api_token=self.api_token) state = {}
if self.api_token:
state['api_token'] = self.api_token
return state
def clear_state(self):
"""clear any state that should be cleared when the process stops
State that should be preserved across server instances should not be cleared.
Subclasses should call super, to ensure that state is properly cleared.
"""
self.api_token = ''
def get_args(self): def get_args(self):
"""Return the arguments to be passed after self.cmd""" """Return the arguments to be passed after self.cmd"""
@@ -208,7 +216,7 @@ class Spawner(LoggingConfigurable):
def wait_for_death(self, timeout=10): def wait_for_death(self, timeout=10):
"""wait for the process to die, up to timeout seconds""" """wait for the process to die, up to timeout seconds"""
loop = IOLoop.current() loop = IOLoop.current()
for i in range(int(timeout / self.poll_interval)): for i in range(int(timeout / self.death_interval)):
status = yield self.poll() status = yield self.poll()
if status is not None: if status is not None:
break break
@@ -291,15 +299,23 @@ class LocalProcessSpawner(Spawner):
raise ValueError("This should be impossible") raise ValueError("This should be impossible")
def load_state(self, state): def load_state(self, state):
"""load pid from state"""
super(LocalProcessSpawner, self).load_state(state) super(LocalProcessSpawner, self).load_state(state)
self.pid = state['pid'] if 'pid' in state:
self.pid = state['pid']
def get_state(self): def get_state(self):
"""add pid to state"""
state = super(LocalProcessSpawner, self).get_state() state = super(LocalProcessSpawner, self).get_state()
if self.pid: if self.pid:
state['pid'] = self.pid state['pid'] = self.pid
return state return state
def clear_state(self):
"""clear pid state"""
super(LocalProcessSpawner, self).clear_state()
self.pid = 0
def sudo_cmd(self, user): def sudo_cmd(self, user):
return ['sudo', '-u', user.name] + self.sudo_args return ['sudo', '-u', user.name] + self.sudo_args
@@ -335,38 +351,39 @@ class LocalProcessSpawner(Spawner):
status = self.proc.poll() status = self.proc.poll()
if status is not None: if status is not None:
# clear state if the process is done # clear state if the process is done
self.pid = 0 self.clear_state()
raise gen.Return(status) raise gen.Return(status)
# if we resumed from stored state, # if we resumed from stored state,
# we don't have the Popen handle anymore # we don't have the Popen handle anymore, so rely on self.pid
if not self.pid: if not self.pid:
# no pid, not running # no pid, not running
self.clear_state()
raise gen.Return(0) raise gen.Return(0)
# send signal 0 to check if PID exists
# this doesn't work on Windows, but that's okay because we don't support Windows. # this doesn't work on Windows, but that's okay because we don't support Windows.
try: alive = self._signal(0)
os.kill(self.pid, 0) if not alive:
except OSError as e: self.clear_state()
if e.errno == errno.ESRCH: raise gen.Return(0)
# no such process, return exitcode == 0, since we don't know the exit status
self.pid = 0
raise gen.Return(0)
else:
raise
else: else:
# None indicates the process is running
raise gen.Return(None) raise gen.Return(None)
def _signal(self, sig): def _signal(self, sig):
"""send a signal, and ignore ERSCH because it just means it already died""" """send a signal, and ignore ERSCH because it just means it already died
returns bool for whether the process existed to receive the signal.
"""
try: try:
os.kill(self.pid, sig) os.kill(self.pid, sig)
except OSError as e: except OSError as e:
if e.errno == errno.ESRCH: if e.errno == errno.ESRCH:
return return False # process is gone
else: else:
raise raise
return True # process exists
@gen.coroutine @gen.coroutine
def stop(self, now=False): def stop(self, now=False):