2020"""
2121from __future__ import absolute_import
2222import six
23+ from six .moves import http_client
2324from six .moves import range
2425
2526__author__ = '[email protected] (Joe Gregorio)' 3637import mimetypes
3738import os
3839import random
40+ import socket
3941import ssl
4042import sys
4143import time
6365
6466MAX_URI_LENGTH = 2048
6567
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+
66113
67114def _retry_request (http , num_retries , req_type , sleep , rand , uri , method , * args ,
68115 ** kwargs ):
@@ -84,21 +131,37 @@ def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
84131 resp, content - Response from the http request (may be HTTP 5xx).
85132 """
86133 resp = None
134+ content = None
87135 for retry_num in range (num_retries + 1 ):
88136 if retry_num > 0 :
89- sleep (rand () * 2 ** retry_num )
137+ # Sleep before retrying.
138+ sleep_time = rand () * 2 ** retry_num
90139 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 )
93144
94145 try :
146+ exception = None
95147 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' , ):
98155 raise
156+ exception = socket_error
157+
158+ if exception :
159+ if retry_num == num_retries :
160+ raise exception
99161 else :
100162 continue
101- if resp .status < 500 :
163+
164+ if not _should_retry_response (resp .status , content ):
102165 break
103166
104167 return resp , content
0 commit comments