20
20
"""
21
21
from __future__ import absolute_import
22
22
import six
23
+ from six .moves import http_client
23
24
from six .moves import range
24
25
25
26
__author__ = '[email protected] (Joe Gregorio)'
36
37
import mimetypes
37
38
import os
38
39
import random
40
+ import socket
39
41
import ssl
40
42
import sys
41
43
import time
63
65
64
66
MAX_URI_LENGTH = 2048
65
67
68
+ _TOO_MANY_REQUESTS = 429
69
+
70
+
71
+ def _should_retry_response (resp_status , content ):
72
+ """Determines whether a response should be retried.
73
+
74
+ Args:
75
+ resp_status: The response status received.
76
+ content: The response content body.
77
+
78
+ Returns:
79
+ True if the response should be retried, otherwise False.
80
+ """
81
+ # Retry on 5xx errors.
82
+ if resp_status >= 500 :
83
+ return True
84
+
85
+ # Retry on 429 errors.
86
+ if resp_status == _TOO_MANY_REQUESTS :
87
+ return True
88
+
89
+ # For 403 errors, we have to check for the `reason` in the response to
90
+ # determine if we should retry.
91
+ if resp_status == six .moves .http_client .FORBIDDEN :
92
+ # If there's no details about the 403 type, don't retry.
93
+ if not content :
94
+ return False
95
+
96
+ # Content is in JSON format.
97
+ try :
98
+ data = json .loads (content .decode ('utf-8' ))
99
+ reason = data ['error' ]['errors' ][0 ]['reason' ]
100
+ except (UnicodeDecodeError , ValueError , KeyError ):
101
+ LOGGER .warning ('Invalid JSON content from response: %s' , content )
102
+ return False
103
+
104
+ LOGGER .warning ('Encountered 403 Forbidden with reason "%s"' , reason )
105
+
106
+ # Only retry on rate limit related failures.
107
+ if reason in ('userRateLimitExceeded' , 'rateLimitExceeded' , ):
108
+ return True
109
+
110
+ # Everything else is a success or non-retriable so break.
111
+ return False
112
+
66
113
67
114
def _retry_request (http , num_retries , req_type , sleep , rand , uri , method , * args ,
68
115
** kwargs ):
@@ -84,21 +131,37 @@ def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
84
131
resp, content - Response from the http request (may be HTTP 5xx).
85
132
"""
86
133
resp = None
134
+ content = None
87
135
for retry_num in range (num_retries + 1 ):
88
136
if retry_num > 0 :
89
- sleep (rand () * 2 ** retry_num )
137
+ # Sleep before retrying.
138
+ sleep_time = rand () * 2 ** retry_num
90
139
LOGGER .warning (
91
- 'Retry #%d for %s: %s %s%s' % (retry_num , req_type , method , uri ,
92
- ', following status: %d' % resp .status if resp else '' ))
140
+ 'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s' ,
141
+ sleep_time , retry_num , num_retries , req_type , method , uri ,
142
+ resp .status if resp else exception )
143
+ sleep (sleep_time )
93
144
94
145
try :
146
+ exception = None
95
147
resp , content = http .request (uri , method , * args , ** kwargs )
96
- except ssl .SSLError :
97
- if retry_num == num_retries :
148
+ # Retry on SSL errors and socket timeout errors.
149
+ except ssl .SSLError as ssl_error :
150
+ exception = ssl_error
151
+ except socket .error as socket_error :
152
+ # errno's contents differ by platform, so we have to match by name.
153
+ if socket .errno .errorcode .get (socket_error .errno ) not in (
154
+ 'WSAETIMEDOUT' , 'ETIMEDOUT' , ):
98
155
raise
156
+ exception = socket_error
157
+
158
+ if exception :
159
+ if retry_num == num_retries :
160
+ raise exception
99
161
else :
100
162
continue
101
- if resp .status < 500 :
163
+
164
+ if not _should_retry_response (resp .status , content ):
102
165
break
103
166
104
167
return resp , content
0 commit comments