6
6
from threading import local , Lock
7
7
8
8
import requests
9
+ from requests .adapters import HTTPAdapter
10
+ from requests .exceptions import RetryError
11
+ from urllib3 .util import Retry
12
+ from http import HTTPStatus
9
13
10
14
try :
11
15
from time import monotonic
32
36
])
33
37
34
38
39
+ class LoggingRetry (Retry ):
40
+ def __init__ (self , * args , ** kwargs , ):
41
+ self ._logger = kwargs .pop ('logger' , None )
42
+ super (LoggingRetry , self ).__init__ (* args , ** kwargs )
43
+
44
+ def new (self , ** kw ):
45
+ kw ['logger' ] = self ._logger
46
+ return super (LoggingRetry , self ).new (** kw )
47
+
48
+ def increment (self , method , url , * args , ** kwargs ):
49
+ response = kwargs .get ("response" )
50
+ if response :
51
+ self ._logger .error ("An invalid status code %s was received "
52
+ "when trying to %s to %s: %s" ,
53
+ response .status , method , url , response .reason )
54
+ else : # pragma: no cover
55
+ self ._logger .error (
56
+ "An unknown error occurred when trying to %s to %s" , method ,
57
+ url )
58
+ return super (LoggingRetry , self ).increment (method , url , * args ,
59
+ ** kwargs )
60
+
61
+
35
62
class FastPurgeError (RuntimeError ):
36
63
"""Raised when the Fast Purge API reports an error.
37
64
@@ -74,6 +101,11 @@ class FastPurgeClient(object):
74
101
# Default network matches Akamai's documented default
75
102
DEFAULT_NETWORK = os .environ .get ("FAST_PURGE_DEFAULT_NETWORK" , "production" )
76
103
104
+ # Max number of retries allowed for HTTP requests, and the backoff used
105
+ # to extend the delay between requests.
106
+ MAX_RETRIES = int (os .environ .get ("FAST_PURGE_MAX_RETRIES" , "10" ))
107
+
108
+ RETRY_BACKOFF = float (os .environ .get ("FAST_PURGE_RETRY_BACKOFF" , "0.15" ))
77
109
# Default purge type.
78
110
# Akamai recommend "invalidate", so why is "delete" our default?
79
111
# Here's what Akamai docs have to say:
@@ -197,12 +229,32 @@ def __baseurl(self):
197
229
198
230
return '{out}:{port}' .format (out = out , port = self .__port )
199
231
232
+ @property
233
+ def __retry_policy (self ):
234
+ retries = getattr (self .__local , 'retries' , None )
235
+ if not retries :
236
+ retries = LoggingRetry (
237
+ total = self .MAX_RETRIES ,
238
+ backoff_factor = self .RETRY_BACKOFF ,
239
+ # We strictly require 201 here since that's how the server
240
+ # tells us we queued something async, as expected
241
+ status_forcelist = [status .value for status in HTTPStatus
242
+ if status .value != 201 ],
243
+ allowed_methods = {'POST' },
244
+ logger = LOG ,
245
+ )
246
+ self .__local .retries = retries
247
+ return retries
248
+
200
249
@property
201
250
def __session (self ):
202
251
session = getattr (self .__local , 'session' , None )
203
252
if not session :
204
253
session = requests .Session ()
205
254
session .auth = EdgeGridAuth (** self .__auth )
255
+ session .mount (self .__baseurl ,
256
+ HTTPAdapter (max_retries = self .__retry_policy ))
257
+
206
258
self .__local .session = session
207
259
return session
208
260
@@ -223,21 +275,16 @@ def __get_request_bodies(self, objects):
223
275
def __start_purge (self , endpoint , request_body ):
224
276
headers = {'Content-Type' : 'application/json' }
225
277
LOG .debug ("POST JSON of size %s to %s" , len (request_body ), endpoint )
226
-
227
- response = self .__session .post (endpoint , data = request_body , headers = headers )
228
-
229
- # Did it succeed? We strictly require 201 here since that's how the server tells
230
- # us we queued something async, as expected
231
- if response . status_code != 201 :
232
- message = "Request to {endpoint} failed: {r.status_code} {r. reason} {text}" . \
233
- format (endpoint = endpoint , r = response , text = response . text [ 0 : 800 ] )
278
+ try :
279
+ response = self .__session .post (endpoint , data = request_body , headers = headers )
280
+ response_body = response . json ()
281
+ estimated_seconds = response_body . get ( 'estimatedSeconds' , 5 )
282
+ return Purge ( response_body , monotonic () + estimated_seconds )
283
+ except RetryError as e :
284
+ message = "Request to {endpoint} was unsuccessful after {retries} retries: { reason}" . \
285
+ format (endpoint = endpoint , retries = self . MAX_RETRIES , reason = e . args [ 0 ]. reason )
234
286
LOG .debug ("%s" , message )
235
- raise FastPurgeError (message )
236
-
237
- response_body = response .json ()
238
- estimated_seconds = response_body .get ('estimatedSeconds' , 5 )
239
-
240
- return Purge (response_body , monotonic () + estimated_seconds )
287
+ raise FastPurgeError (message ) from e
241
288
242
289
def purge_objects (self , object_type , objects , ** kwargs ):
243
290
"""Purge a collection of objects.
0 commit comments