diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 5ebecb68..81a94249 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -534,11 +534,11 @@ class JupyterHub(Application): @default('authenticator') def _authenticator_default(self): return self.authenticator_class(parent=self, db=self.db) - + allow_named_servers = Bool(False, help="Allow named single-user servers per user" ).tag(config=True) - + # class for spawning single-user servers spawner_class = Type(LocalProcessSpawner, Spawner, help="""The class to use for spawning single-user servers. @@ -547,6 +547,18 @@ class JupyterHub(Application): """ ).tag(config=True) + concurrent_spawn_limit = Integer( + 0, + help=""" + Maximum number of concurrent users that can be spawning at a time. + + If more than this many users attempt to spawn at a time, their + request is rejected with a 429 error asking them to try again. + + If set to 0, no concurrent_spawn_limit is enforced. + """ + ).tag(config=True) + db_url = Unicode('sqlite:///jupyterhub.sqlite', help="url for the database. e.g. `sqlite:///jupyterhub.sqlite`" ).tag(config=True) @@ -1257,6 +1269,7 @@ class JupyterHub(Application): statsd=self.statsd, allow_named_servers=self.allow_named_servers, oauth_provider=self.oauth_provider, + concurrent_spawn_limit=self.concurrent_spawn_limit, ) # allow configured settings to have priority settings.update(self.tornado_settings) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 75fb161e..6f215701 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -367,6 +367,19 @@ class BaseHandler(RequestHandler): 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) + + concurrent_spawn_limit = self.settings['concurrent_spawn_limit'] + if concurrent_spawn_limit and self.settings.get('_spawn_pending_count', 0) > concurrent_spawn_limit: + self.log.info( + 'More than %s pending spawns, throttling', + self.settings['concurrent_spawn_limit'] + ) + raise web.HTTPError( + 429, + "User startup rate limit exceeded. Try to start again in a few minutes.") + + # FIXME: Move this out of settings, since this isn't really a setting + self.settings['_spawn_pending_count'] = self.settings.get('_spawn_pending_count', 0) + 1 tic = IOLoop.current().time() user_server_name = user.name if server_name: @@ -403,6 +416,7 @@ class BaseHandler(RequestHandler): spawner.add_poll_callback(self.user_stopped, user) finally: spawner._proxy_pending = False + self.settings['_spawn_pending_count'] -= 1 try: yield gen.with_timeout(timedelta(seconds=self.slow_spawn_timeout), f)