Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 13d08b9

Browse files
committed
retries/timeouts
1 parent 05d5f92 commit 13d08b9

File tree

5 files changed

+109
-57
lines changed

5 files changed

+109
-57
lines changed

auth0/v3/management/asyncify.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ def __init__(self, *args, **kwargs):
4848
super(Wrapper, self).__init__(*args, **kwargs)
4949
self._async_client = AsyncClient(*args, **kwargs)
5050
for method in methods:
51-
setattr(self, f"{method}_async", _gen_async(self._async_client, method))
51+
setattr(
52+
self,
53+
"{}_async".format(method),
54+
_gen_async(self._async_client, method),
55+
)
5256

5357
async def __aenter__(self):
5458
"""Automatically create and set session within context manager."""

auth0/v3/management/rest.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ def __init__(self, jwt, telemetry=True, timeout=5.0, options=None):
100100
}
101101
)
102102

103+
# Cap the maximum number of retries to 10 or fewer. Floor the retries at 0.
104+
self._retries = min(self.MAX_REQUEST_RETRIES(), max(0, options.retries))
105+
103106
# For backwards compatibility reasons only
104107
# TODO: Deprecate in the next major so we can prune these arguments. Guidance should be to use RestClient.options.*
105108
self.telemetry = options.telemetry
@@ -130,9 +133,6 @@ def get(self, url, params=None):
130133
# Reset the metrics tracker
131134
self._metrics = {"retries": 0, "backoff": []}
132135

133-
# Cap the maximum number of retries to 10 or fewer. Floor the retries at 0.
134-
retries = min(self.MAX_REQUEST_RETRIES(), max(0, self.options.retries))
135-
136136
while True:
137137
# Increment attempt number
138138
attempt += 1
@@ -142,27 +142,11 @@ def get(self, url, params=None):
142142
url, params=params, headers=headers, timeout=self.options.timeout
143143
)
144144

145-
# If the response did not have a 429 header, or the retries were configured at 0, or the attempt number is equal to or greater than the configured retries, break
146-
if response.status_code != 429 or retries <= 0 or attempt > retries:
145+
# If the response did not have a 429 header, or the attempt number is greater than the configured retries, break
146+
if response.status_code != 429 or attempt > self._retries:
147147
break
148148

149-
# Retry the request. Apply a exponential backoff for subsequent attempts, using this formula:
150-
# max(MIN_REQUEST_RETRY_DELAY, min(MAX_REQUEST_RETRY_DELAY, (100ms * (2 ** attempt - 1)) + random_between(1, MAX_REQUEST_RETRY_JITTER)))
151-
152-
# Increases base delay by (100ms * (2 ** attempt - 1))
153-
wait = 100 * 2 ** (attempt - 1)
154-
155-
# Introduces jitter to the base delay; increases delay between 1ms to MAX_REQUEST_RETRY_JITTER (100ms)
156-
wait += randint(1, self.MAX_REQUEST_RETRY_JITTER())
157-
158-
# Is never more than MAX_REQUEST_RETRY_DELAY (1s)
159-
wait = min(self.MAX_REQUEST_RETRY_DELAY(), wait)
160-
161-
# Is never less than MIN_REQUEST_RETRY_DELAY (100ms)
162-
wait = max(self.MIN_REQUEST_RETRY_DELAY(), wait)
163-
164-
self._metrics["retries"] = attempt
165-
self._metrics["backoff"].append(wait)
149+
wait = self._calculate_wait(attempt)
166150

167151
# Skip calling sleep() when running unit tests
168152
if self._skip_sleep is False:
@@ -217,6 +201,27 @@ def delete(self, url, params=None, data=None):
217201
)
218202
return self._process_response(response)
219203

204+
def _calculate_wait(self, attempt):
205+
# Retry the request. Apply a exponential backoff for subsequent attempts, using this formula:
206+
# max(MIN_REQUEST_RETRY_DELAY, min(MAX_REQUEST_RETRY_DELAY, (100ms * (2 ** attempt - 1)) + random_between(1, MAX_REQUEST_RETRY_JITTER)))
207+
208+
# Increases base delay by (100ms * (2 ** attempt - 1))
209+
wait = 100 * 2 ** (attempt - 1)
210+
211+
# Introduces jitter to the base delay; increases delay between 1ms to MAX_REQUEST_RETRY_JITTER (100ms)
212+
wait += randint(1, self.MAX_REQUEST_RETRY_JITTER())
213+
214+
# Is never more than MAX_REQUEST_RETRY_DELAY (1s)
215+
wait = min(self.MAX_REQUEST_RETRY_DELAY(), wait)
216+
217+
# Is never less than MIN_REQUEST_RETRY_DELAY (100ms)
218+
wait = max(self.MIN_REQUEST_RETRY_DELAY(), wait)
219+
220+
self._metrics["retries"] = attempt
221+
self._metrics["backoff"].append(wait)
222+
223+
return wait
224+
220225
def _process_response(self, response):
221226
return self._parse(response).content()
222227

auth0/v3/management/rest_async.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import aiohttp
55

6+
from ..exceptions import RateLimitError
67
from .rest import EmptyResponse, JsonResponse, PlainResponse
78
from .rest import Response as _Response
89
from .rest import RestClient
@@ -34,6 +35,14 @@ class AsyncRestClient(RestClient):
3435
def __init__(self, *args, **kwargs):
3536
super(AsyncRestClient, self).__init__(*args, **kwargs)
3637
self._session = None
38+
sock_connect, sock_read = (
39+
self.timeout
40+
if isinstance(self.timeout, tuple)
41+
else (self.timeout, self.timeout)
42+
)
43+
self.timeout = aiohttp.ClientTimeout(
44+
sock_connect=sock_connect, sock_read=sock_read
45+
)
3746

3847
def set_session(self, session):
3948
"""Set Client Session to improve performance by reusing session.
@@ -42,8 +51,8 @@ def set_session(self, session):
4251
self._session = session
4352

4453
async def _request(self, *args, **kwargs):
45-
# FIXME add support for timeouts
4654
kwargs["headers"] = kwargs.get("headers", self.base_headers)
55+
kwargs["timeout"] = self.timeout
4756
if self._session is not None:
4857
# Request with re-usable session
4958
async with self._session.request(*args, **kwargs) as response:
@@ -55,7 +64,31 @@ async def _request(self, *args, **kwargs):
5564
return await self._process_response(response)
5665

5766
async def get(self, url, params=None):
58-
return await self._request("get", url, params=_clean_params(params))
67+
# Track the API request attempt number
68+
attempt = 0
69+
70+
# Reset the metrics tracker
71+
self._metrics = {"retries": 0, "backoff": []}
72+
73+
params = _clean_params(params)
74+
while True:
75+
# Increment attempt number
76+
attempt += 1
77+
78+
try:
79+
response = await self._request("get", url, params=params)
80+
return response
81+
except RateLimitError as e:
82+
# If the attempt number is greater than the configured retries, raise RateLimitError
83+
if attempt > self._retries:
84+
raise e
85+
86+
wait = self._calculate_wait(attempt)
87+
88+
# Skip calling sleep() when running unit tests
89+
if self._skip_sleep is False:
90+
# sleep() functions in seconds, so convert the milliseconds formula above accordingly
91+
await asyncio.sleep(wait / 1000)
5992

6093
async def post(self, url, data=None):
6194
return await self._request("post", url, json=data)

auth0/v3/management/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
def is_async_available():
5-
if sys.version_info >= (3, 0):
5+
if sys.version_info >= (3, 6):
66
try:
77
import asyncio
88

auth0/v3/test_async/test_asyncify.py

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from tempfile import TemporaryFile
88
from unittest import IsolatedAsyncioTestCase
99

10+
import aiohttp
1011
from aioresponses import CallbackResult, aioresponses
1112
from callee import Attrs
1213
from mock import ANY, MagicMock
@@ -31,15 +32,15 @@
3132
).decode()
3233

3334
headers = {
34-
"User-Agent": f"Python/{platform.python_version()}",
35+
"User-Agent": "Python/{}".format(platform.python_version()),
3536
"Authorization": "Bearer jwt",
3637
"Content-Type": "application/json",
3738
"Auth0-Client": telemetry,
3839
}
3940

4041

41-
def get_callback():
42-
mock = MagicMock(return_value=CallbackResult(status=200, payload=payload))
42+
def get_callback(status=200):
43+
mock = MagicMock(return_value=CallbackResult(status=status, payload=payload))
4344

4445
def callback(url, **kwargs):
4546
return mock(url, **kwargs)
@@ -52,10 +53,7 @@ class TestAsyncify(IsolatedAsyncioTestCase):
5253
async def test_get(self, mocked):
5354
callback, mock = get_callback()
5455
mocked.get(clients, callback=callback)
55-
c = asyncify(Clients)(
56-
domain="example.com",
57-
token="jwt",
58-
)
56+
c = asyncify(Clients)(domain="example.com", token="jwt")
5957
self.assertEqual(await c.all_async(), payload)
6058
mock.assert_called_with(
6159
Attrs(path="/api/v2/clients"),
@@ -68,10 +66,7 @@ async def test_get(self, mocked):
6866
async def test_post(self, mocked):
6967
callback, mock = get_callback()
7068
mocked.post(clients, callback=callback)
71-
c = asyncify(Clients)(
72-
domain="example.com",
73-
token="jwt",
74-
)
69+
c = asyncify(Clients)(domain="example.com", token="jwt")
7570
data = {"client": 1}
7671
self.assertEqual(await c.create_async(data), payload)
7772
mock.assert_called_with(
@@ -85,10 +80,7 @@ async def test_post(self, mocked):
8580
async def test_file_post(self, mocked):
8681
callback, mock = get_callback()
8782
mocked.post(users_imports, callback=callback)
88-
j = asyncify(Jobs)(
89-
domain="example.com",
90-
token="jwt",
91-
)
83+
j = asyncify(Jobs)(domain="example.com", token="jwt")
9284
users = TemporaryFile()
9385
self.assertEqual(await j.import_users_async("connection-1", users), payload)
9486
file_port_headers = headers.copy()
@@ -105,15 +97,13 @@ async def test_file_post(self, mocked):
10597
},
10698
headers=file_port_headers,
10799
)
100+
users.close()
108101

109102
@aioresponses()
110103
async def test_patch(self, mocked):
111104
callback, mock = get_callback()
112105
mocked.patch(clients, callback=callback)
113-
c = asyncify(Clients)(
114-
domain="example.com",
115-
token="jwt",
116-
)
106+
c = asyncify(Clients)(domain="example.com", token="jwt")
117107
data = {"client": 1}
118108
self.assertEqual(await c.update_async("client-1", data), payload)
119109
mock.assert_called_with(
@@ -127,10 +117,7 @@ async def test_patch(self, mocked):
127117
async def test_put(self, mocked):
128118
callback, mock = get_callback()
129119
mocked.put(factors, callback=callback)
130-
g = asyncify(Guardian)(
131-
domain="example.com",
132-
token="jwt",
133-
)
120+
g = asyncify(Guardian)(domain="example.com", token="jwt")
134121
data = {"factor": 1}
135122
self.assertEqual(await g.update_factor_async("factor-1", data), payload)
136123
mock.assert_called_with(
@@ -144,10 +131,7 @@ async def test_put(self, mocked):
144131
async def test_delete(self, mocked):
145132
callback, mock = get_callback()
146133
mocked.delete(clients, callback=callback)
147-
c = asyncify(Clients)(
148-
domain="example.com",
149-
token="jwt",
150-
)
134+
c = asyncify(Clients)(domain="example.com", token="jwt")
151135
self.assertEqual(await c.delete_async("client-1"), payload)
152136
mock.assert_called_with(
153137
Attrs(path="/api/v2/clients/client-1"),
@@ -161,14 +145,40 @@ async def test_delete(self, mocked):
161145
async def test_shared_session(self, mocked):
162146
callback, mock = get_callback()
163147
mocked.get(clients, callback=callback)
164-
async with asyncify(Clients)(
165-
domain="example.com",
166-
token="jwt",
167-
) as c:
148+
async with asyncify(Clients)(domain="example.com", token="jwt") as c:
168149
self.assertEqual(await c.all_async(), payload)
169150
mock.assert_called_with(
170151
Attrs(path="/api/v2/clients"),
171152
allow_redirects=True,
172153
params={"include_fields": "true"},
173154
headers=headers,
174155
)
156+
157+
@aioresponses()
158+
async def test_rate_limit(self, mocked):
159+
callback, mock = get_callback(status=429)
160+
mocked.get(clients, callback=callback)
161+
mocked.get(clients, callback=callback)
162+
mocked.get(clients, callback=callback)
163+
mocked.get(clients, payload=payload)
164+
c = asyncify(Clients)(domain="example.com", token="jwt")
165+
rest_client = c._async_client.client
166+
rest_client._skip_sleep = True
167+
self.assertEqual(await c.all_async(), payload)
168+
self.assertEqual(3, mock.call_count)
169+
(a, b, c) = rest_client._metrics["backoff"]
170+
self.assertTrue(100 <= a < b < c <= 1000)
171+
172+
@aioresponses()
173+
async def test_timeout(self, mocked):
174+
callback, mock = get_callback()
175+
mocked.get(clients, callback=callback)
176+
c = asyncify(Clients)(domain="example.com", token="jwt", timeout=(8.8, 9.9))
177+
self.assertEqual(await c.all_async(), payload)
178+
mock.assert_called_with(
179+
ANY,
180+
allow_redirects=ANY,
181+
params=ANY,
182+
headers=ANY,
183+
timeout=aiohttp.ClientTimeout(sock_connect=8.8, sock_read=9.9),
184+
)

0 commit comments

Comments
 (0)