create Spawners table

for named servers

removes User.servers
This commit is contained in:
Min RK
2017-07-18 14:28:31 +02:00
parent 9eef5d7b1e
commit a2e94b8493
7 changed files with 73 additions and 78 deletions

View File

@@ -1129,29 +1129,30 @@ class JupyterHub(Application):
for orm_user in db.query(orm.User):
self.users[orm_user.id] = user = User(orm_user, self.tornado_settings)
self.log.debug("Loading state for %s from db", user.name)
spawner = user.spawner
status = 0
if user.server:
try:
status = yield spawner.poll()
except Exception:
self.log.exception("Failed to poll spawner for %s, assuming the spawner is not running.", user.name)
status = -1
for name, spawner in user.spawners.items():
status = 0
if spawner.server:
try:
status = yield spawner.poll()
except Exception:
self.log.exception("Failed to poll spawner for %s, assuming the spawner is not running.",
user.name if name else '%s|%s' % (user.name, name))
status = -1
if status is None:
self.log.info("%s still running", user.name)
spawner.add_poll_callback(user_stopped, user)
spawner.start_polling()
else:
# user not running. This is expected if server is None,
# but indicates the user's server died while the Hub wasn't running
# if user.server is defined.
log = self.log.warning if user.server else self.log.debug
log("%s not running.", user.name)
# remove all server or servers entry from db related to the user
for server in user.servers:
db.delete(server)
db.commit()
if status is None:
self.log.info("%s still running", user.name)
spawner.add_poll_callback(user_stopped, user)
spawner.start_polling()
else:
# user not running. This is expected if server is None,
# but indicates the user's server died while the Hub wasn't running
# if user.server is defined.
log = self.log.warning if spawner.server else self.log.debug
log("%s not running.", user.name)
# remove all server or servers entry from db related to the user
if spawner.server:
db.delete(spawner.orm_spawner.server)
db.commit()
user_summaries.append(_user_summary(user))

View File

@@ -66,7 +66,6 @@ class Server(Base):
__tablename__ = 'servers'
id = Column(Integer, primary_key=True)
name = Column(Unicode(32), default='') # must be unique between user's servers
proto = Column(Unicode(15), default='http')
ip = Column(Unicode(255), default='') # could also be a DNS name
port = Column(Integer, default=random_port)
@@ -133,7 +132,10 @@ class User(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(1023), unique=True)
servers = association_proxy("user_to_servers", "server", creator=lambda server: UserServer(server=server))
_orm_spawners = relationship("Spawner", backref="user")
@property
def orm_spawners(self):
return {s.name: s for s in self._orm_spawners}
admin = Column(Boolean, default=False)
last_activity = Column(DateTime, default=datetime.utcnow)
@@ -149,19 +151,12 @@ class User(Base):
groups = relationship('Group', secondary='user_group_map', back_populates='users')
def __repr__(self):
if self.servers:
server = self.servers[0]
return "<{cls}({name}@{ip}:{port})>".format(
cls=self.__class__.__name__,
name=self.name,
ip=server.ip,
port=server.port,
)
else:
return "<{cls}({name} [unconfigured])>".format(
cls=self.__class__.__name__,
name=self.name,
)
return "<{cls}({name} {running}/{total} running)>".format(
cls=self.__class__.__name__,
name=self.name,
total=len(self._orm_spawners),
running=sum(bool(s.server) for s in self._orm_spawners),
)
def new_api_token(self, token=None):
"""Create a new API token
@@ -177,34 +172,18 @@ class User(Base):
"""
return db.query(cls).filter(cls.name == name).first()
class Spawner(Base):
""""State about a Spawner"""
__tablename__ = 'spawners'
class UserServer(Base):
"""The UserServer table
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
A table storing the One-To-Many relationship between a user and servers.
Each user may have one or more servers.
A server can have only one (1) user. This condition is maintained by UniqueConstraint.
"""
__tablename__ = 'users_servers'
server_id = Column(Integer, ForeignKey('servers.id'))
server = relationship(Server)
_user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
_server_id = Column(Integer, ForeignKey('servers.id'), primary_key=True)
user = relationship(User, backref=backref('user_to_servers', cascade='all, delete-orphan'))
server = relationship(Server, backref=backref('server_to_users', cascade='all, delete-orphan')
)
__table_args__ = (UniqueConstraint('_server_id'),
Index('server_user_index', '_server_id', '_user_id'),
)
def __repr__(self):
return "<{cls}({name}@{ip}:{port})>".format(
cls=self.__class__.__name__,
name=self.user.name,
ip=self.server.ip,
port=self.server.port,
)
state = Column(JSONDict)
name = Column(Unicode(512))
class Service(Base):

View File

@@ -51,6 +51,7 @@ class Spawner(LoggingConfigurable):
_stop_pending = False
_waiting_for_response = False
orm_spawner = Any()
db = Any()
user = Any()
hub = Any()

View File

@@ -405,7 +405,7 @@ def test_spawn(app, io_loop):
data=json.dumps(options),
)
assert r.status_code == 201
assert 'pid' in user.state['']
assert 'pid' in user.orm_spawners[''].state
app_user = get_app_user(app, name)
assert app_user.spawner is not None
assert app_user.spawner.user_options == options
@@ -433,7 +433,7 @@ def test_spawn(app, io_loop):
r = api_request(app, 'users', name, 'server', method='delete')
assert r.status_code == 204
assert 'pid' not in user.state.get('', {})
assert 'pid' not in user.orm_spawners[''].state
status = io_loop.run_sync(app_user.spawner.poll)
assert status == 0
@@ -531,7 +531,7 @@ def test_cookie(app):
user = add_user(db, app=app, name=name)
r = api_request(app, 'users', name, 'server', method='post')
assert r.status_code == 201
assert 'pid' in user.state['']
assert 'pid' in user.orm_spawners[''].state
app_user = get_app_user(app, name)
cookies = app.login_user(name)

View File

@@ -153,8 +153,8 @@ def test_spawner_poll(db, io_loop):
assert status is None
if user.state is None:
user.state = {}
user.state[''] = first_spawner.get_state()
assert 'pid' in user.state['']
first_spawner.orm_spawner.state = first_spawner.get_state()
assert 'pid' in first_spawner.orm_spawner.state
# create a new Spawner, loading from state of previous
spawner = new_spawner(db, user=first_spawner.user)

View File

@@ -147,17 +147,34 @@ class User(HasTraits):
self.settings.get('base_url', '/'), 'user', self.escaped_name) + '/'
self.spawners = _SpawnerDict(self._new_spawner)
# load existing named spawners
for name in self.orm_spawners:
self.log.debug("Loading spawner %s:%s", self.name, name)
self.spawners[name] = self._new_spawner(name)
def _new_spawner(self, name):
"""Create a new spawner"""
orm_spawner = self.orm_spawners.get(name)
if orm_spawner is None:
orm_spawner = orm.Spawner(user=self.orm_user, name=name)
self.db.add(orm_spawner)
self.db.commit()
assert name in self.orm_spawners
if name == '' and self.state:
# migrate user.state to spawner.state
orm_spawner.state = self.state
self.state = None
spawner = self.spawner_class(
user=self,
db=self.db,
orm_spawner=orm_spawner,
hub=self.settings.get('hub'),
authenticator=self.authenticator,
config=self.settings.get('config'),
)
spawner.load_state((self.state or {}).get(name, {}))
if orm_spawner.server:
spawner.server = Server(orm_spawner.server)
spawner.load_state(orm_spawner.state or {})
return spawner
# singleton property, self.spawner maps onto spawner with empty server_name
@@ -198,10 +215,7 @@ class User(HasTraits):
@property
def server(self):
if len(self.servers) == 0:
return None
else:
return Server(orm_server=self.servers[0])
return self.spawner.server
@property
def escaped_name(self):
@@ -264,10 +278,9 @@ class User(HasTraits):
base_url = url_path_join(self.base_url, server_name) + '/'
orm_server = orm.Server(
name=server_name,
base_url=base_url,
)
self.servers.append(orm_server)
db.add(orm_server)
api_token = self.new_api_token()
db.commit()
@@ -275,6 +288,7 @@ class User(HasTraits):
server = Server(orm_server=orm_server)
spawner = self.spawners[server_name]
spawner.orm_spawner.server = orm_server
# Passing server, server_name and options to the spawner
spawner.server = server
@@ -353,7 +367,7 @@ class User(HasTraits):
# store state
if self.state is None:
self.state = {}
self.state[server_name] = spawner.get_state()
spawner.orm_spawner.state = spawner.get_state()
self.last_activity = datetime.utcnow()
db.commit()
spawner._waiting_for_response = True
@@ -407,7 +421,7 @@ class User(HasTraits):
if status is None:
yield spawner.stop()
spawner.clear_state()
self.state = spawner.get_state()
spawner.orm_spawner.state = spawner.get_state()
self.last_activity = datetime.utcnow()
# remove server entry from db
if spawner.server is not None:

View File

@@ -225,7 +225,7 @@ def default_server_name(user):
Will be the first available integer string, e.g. '1' or '2'.
"""
existing_names = { server.name for server in user.servers }
existing_names = set(user.spawners)
# if there are 5 servers, count from 1 to 6
for n in range(1, len(existing_names) + 2):
name = str(n)