address review in spawn-throttle-retry

- update config to single tuple instead of two integers
- call it spawn_throttle_retry_range
- fix setting Retry-After header without disabling error pages
This commit is contained in:
Min RK
2018-05-04 13:44:32 +02:00
parent 8898faa141
commit 09a8fd5254
3 changed files with 56 additions and 35 deletions

View File

@@ -72,6 +72,7 @@ class APIHandler(BaseHandler):
"""Write JSON errors instead of HTML""" """Write JSON errors instead of HTML"""
exc_info = kwargs.get('exc_info') exc_info = kwargs.get('exc_info')
message = '' message = ''
exception = None
status_message = responses.get(status_code, 'Unknown Error') status_message = responses.get(status_code, 'Unknown Error')
if exc_info: if exc_info:
exception = exc_info[1] exception = exc_info[1]
@@ -85,6 +86,15 @@ class APIHandler(BaseHandler):
reason = getattr(exception, 'reason', '') reason = getattr(exception, 'reason', '')
if reason: if reason:
status_message = reason status_message = reason
self.set_header('Content-Type', 'application/json')
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
self.write(json.dumps({ self.write(json.dumps({
'status': status_code, 'status': status_code,
'message': message or status_message, 'message': message or status_message,

View File

@@ -36,7 +36,7 @@ from tornado.platform.asyncio import AsyncIOMainLoop
from traitlets import ( from traitlets import (
Unicode, Integer, Dict, TraitError, List, Bool, Any, Unicode, Integer, Dict, TraitError, List, Bool, Any,
Type, Set, Instance, Bytes, Float, Tuple, Type, Set, Instance, Bytes, Float,
observe, default, observe, default,
) )
from traitlets.config import Application, catch_config_error from traitlets.config import Application, catch_config_error
@@ -616,28 +616,20 @@ class JupyterHub(Application):
""" """
).tag(config=True) ).tag(config=True)
throttle_retry_suggest_min = Integer( spawn_throttle_retry_range = Tuple(
30, (30, 60),
help=""" help="""
Minimum seconds after which we suggest the user retry spawning. (min seconds, max seconds) range to suggest a user wait before retrying.
When `concurrent_spawn_limit` is exceeded, we recommend users retry When `concurrent_spawn_limit` is exceeded, spawning is throttled.
after a random period of time, bounded by throttle_retry_suggest_min We suggest users wait random period of time within this range
and throttle_retry_suggest_max. before retrying.
throttle_retry_suggest_min should ideally be set to the median A Retry-After header is set with a random value within this range.
spawn time of servers in your installation. Error pages will display a rounded version of this value.
"""
)
throttle_retry_suggest_max = Integer( The lower bound should ideally be approximately
60, the median spawn time for your deployment.
help="""
Minimum seconds after which we suggest the user retry spawning.
When `concurrent_spawn_limit` is exceeded, we recommend users retry
after a random period of time, bounded by throttle_retry_suggest_min
and throttle_retry_suggest_max.
""" """
) )
@@ -1448,8 +1440,7 @@ class JupyterHub(Application):
allow_named_servers=self.allow_named_servers, allow_named_servers=self.allow_named_servers,
oauth_provider=self.oauth_provider, oauth_provider=self.oauth_provider,
concurrent_spawn_limit=self.concurrent_spawn_limit, concurrent_spawn_limit=self.concurrent_spawn_limit,
throttle_retry_suggest_min=self.throttle_retry_suggest_min, spawn_throttle_retry_range=self.spawn_throttle_retry_range,
throttle_retry_suggest_max=self.throttle_retry_suggest_max,
active_server_limit=self.active_server_limit, active_server_limit=self.active_server_limit,
) )
# allow configured settings to have priority # allow configured settings to have priority

View File

@@ -4,13 +4,14 @@
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import copy import copy
import re
import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from http.client import responses from http.client import responses
import math
import random
import re
import time
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
import uuid import uuid
import random
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
@@ -533,18 +534,30 @@ class BaseHandler(RequestHandler):
# Suggest number of seconds client should wait before retrying # Suggest number of seconds client should wait before retrying
# This helps prevent thundering herd problems, where users simply # This helps prevent thundering herd problems, where users simply
# immediately retry when we are overloaded. # immediately retry when we are overloaded.
retry_time = int(random.uniform( retry_range = self.settings['spawn_throttle_retry_range']
self.settings['throttle_retry_suggest_min'], retry_time = int(random.uniform(*retry_range))
self.settings['throttle_retry_suggest_max']
)) # round suggestion to nicer human value (nearest 10 seconds or minute)
self.set_header('Retry-After', str(retry_time)) if retry_time <= 90:
self.log.info( # round human seconds up to nearest 10
'%s pending spawns, throttling. Retry in %s seconds', human_retry_time = "%i0 seconds" % math.ceil(retry_time / 10.)
spawn_pending_count, retry_time else:
# round number of minutes
human_retry_time = "%i minutes" % math.round(retry_time / 60.)
self.log.warning(
'%s pending spawns, throttling. Suggested retry in %s seconds.',
spawn_pending_count, retry_time,
) )
self.set_status(429, "Too many users trying to log in right now. Try again in a {}s".format(retry_time)) err = web.HTTPError(
# We use set_status and then raise web.Finish, since raising web.HTTPError resets any headers we wanna send. 429,
raise web.Finish() "Too many users trying to log in right now. Try again in {}.".format(human_retry_time)
)
# can't call set_header directly here because it gets ignored
# when errors are raised
# we handle err.headers ourselves in Handler.write_error
err.headers = {'Retry-After': retry_time}
raise err
if active_server_limit and active_count >= active_server_limit: if active_server_limit and active_count >= active_server_limit:
self.log.info( self.log.info(
@@ -779,6 +792,13 @@ class BaseHandler(RequestHandler):
) )
self.set_header('Content-Type', 'text/html') self.set_header('Content-Type', 'text/html')
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
# render the template # render the template
try: try:
html = self.render_template('%s.html' % status_code, **ns) html = self.render_template('%s.html' % status_code, **ns)