18
18
# Copyright (c) Jupyter Development Team.
19
19
# Distributed under the terms of the Modified BSD License.
20
20
21
+ import asyncio
21
22
import json
22
23
import os
23
24
from os .path import abspath
27
28
from jupyterhub .proxy import Proxy
28
29
from jupyterhub .utils import exponential_backoff , new_token , url_path_join
29
30
from tornado .httpclient import AsyncHTTPClient , HTTPClientError
30
- from traitlets import Any , Bool , Dict , Integer , Unicode , default , validate
31
+ from traitlets import Any , Bool , Dict , Integer , Unicode , default , observe , validate
31
32
32
33
from . import traefik_utils
33
34
@@ -37,6 +38,26 @@ class TraefikProxy(Proxy):
37
38
38
39
traefik_process = Any ()
39
40
41
+ concurrency = Integer (
42
+ 10 ,
43
+ config = True ,
44
+ help = """
45
+ The number of requests allowed to be concurrently outstanding to the proxy
46
+
47
+ Limiting this number avoids potential timeout errors
48
+ by sending too many requests to update the proxy at once
49
+ """ ,
50
+ )
51
+ semaphore = Any ()
52
+
53
+ @default ('semaphore' )
54
+ def _default_semaphore (self ):
55
+ return asyncio .BoundedSemaphore (self .concurrency )
56
+
57
+ @observe ('concurrency' )
58
+ def _concurrency_changed (self , change ):
59
+ self .semaphore = asyncio .BoundedSemaphore (change .new )
60
+
40
61
static_config_file = Unicode (
41
62
"traefik.toml" , config = True , help = """traefik's static configuration file"""
42
63
)
@@ -86,6 +107,20 @@ def __init__(self, **kwargs):
86
107
static_config = Dict ()
87
108
dynamic_config = Dict ()
88
109
110
+ traefik_providers_throttle_duration = Unicode (
111
+ "0s" ,
112
+ config = True ,
113
+ help = """
114
+ throttle traefik reloads of configuration.
115
+
116
+ When traefik sees a change in configuration,
117
+ it will wait this long before applying the next one.
118
+ This affects how long adding a user to the proxy will take.
119
+
120
+ See https://doc.traefik.io/traefik/providers/overview/#providersprovidersthrottleduration
121
+ """ ,
122
+ )
123
+
89
124
traefik_api_url = Unicode (
90
125
"http://localhost:8099" ,
91
126
config = True ,
@@ -235,17 +270,18 @@ async def _check_for_traefik_service(self, routespec, kind):
235
270
expected = (
236
271
traefik_utils .generate_alias (routespec , kind ) + "@" + self .provider_name
237
272
)
238
- path = f"/api/http/{ kind } s"
273
+ path = f"/api/http/{ kind } s/ { expected } "
239
274
try :
240
275
resp = await self ._traefik_api_request (path )
241
- json_data = json .loads (resp .body )
242
- except Exception :
276
+ json .loads (resp .body )
277
+ except HTTPClientError as e :
278
+ if e .code == 404 :
279
+ self .log .debug (f"traefik { expected } not yet in { kind } " )
280
+ return False
243
281
self .log .exception (f"Error checking traefik api for { kind } { routespec } " )
244
282
return False
245
-
246
- service_names = [service ['name' ] for service in json_data ]
247
- if expected not in service_names :
248
- self .log .debug (f"traefik { expected } not yet in { kind } " )
283
+ except Exception :
284
+ self .log .exception (f"Error checking traefik api for { kind } { routespec } " )
249
285
return False
250
286
251
287
# found the expected endpoint
@@ -266,6 +302,7 @@ async def _check_traefik_dynamic_conf_ready():
266
302
await exponential_backoff (
267
303
_check_traefik_dynamic_conf_ready ,
268
304
f"Traefik route for { routespec } configuration not available" ,
305
+ scale_factor = 1.2 ,
269
306
timeout = self .check_route_timeout ,
270
307
)
271
308
@@ -358,6 +395,9 @@ async def _setup_traefik_static_config(self):
358
395
Subclasses should specify any traefik providers themselves, in
359
396
:attrib:`self.static_config["providers"]`
360
397
"""
398
+ self .static_config ["providers" ][
399
+ "providersThrottleDuration"
400
+ ] = self .traefik_providers_throttle_duration
361
401
362
402
if self .traefik_log_level :
363
403
self .static_config ["log" ] = {"level" : self .traefik_log_level }
0 commit comments