Skip to content

Commit fb55350

Browse files
feat: improved handling of retries (#188)
Co-authored-by: GMorris-professional <[email protected]>
1 parent a5425ac commit fb55350

File tree

6 files changed

+573
-14
lines changed

6 files changed

+573
-14
lines changed

openfga_sdk/api_client.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
DEFAULT_USER_AGENT = "openfga-sdk python/0.9.3"
4343

4444

45-
def random_time(loop_count, min_wait_in_ms):
45+
def random_time(loop_count, min_wait_in_ms) -> float:
4646
"""
4747
Helper function to return the time (in s) to wait before retry
4848
"""
@@ -253,11 +253,21 @@ async def __call_api(
253253
)
254254
else 0
255255
)
256+
max_wait_in_sec = (
257+
self.configuration.retry_params.max_wait_in_sec
258+
if (
259+
self.configuration.retry_params is not None
260+
and self.configuration.retry_params.max_wait_in_sec is not None
261+
)
262+
else 120
263+
)
256264
if _retry_params is not None:
257265
if _retry_params.max_retry is not None:
258266
max_retry = _retry_params.max_retry
259267
if _retry_params.min_wait_in_ms is not None:
260268
max_retry = _retry_params.min_wait_in_ms
269+
if _retry_params.max_wait_in_sec is not None:
270+
max_wait_in_sec = _retry_params.max_wait_in_sec
261271

262272
_telemetry_attributes = TelemetryAttributes.fromRequest(
263273
user_agent=self.user_agent,
@@ -300,7 +310,14 @@ async def __call_api(
300310
configuration=self.configuration.telemetry,
301311
)
302312

303-
await asyncio.sleep(random_time(retry, min_wait_in_ms))
313+
try:
314+
wait_time_in_sec = self._parse_retry_after_header(e.header)
315+
except ValueError:
316+
wait_time_in_sec = min(
317+
random_time(retry, min_wait_in_ms), max_wait_in_sec
318+
)
319+
320+
await asyncio.sleep(wait_time_in_sec)
304321

305322
continue
306323
e.body = e.body.decode("utf-8")
@@ -395,6 +412,25 @@ async def __call_api(
395412
else:
396413
return (return_data, response_data.status, response_data.headers)
397414

415+
def _parse_retry_after_header(self, headers) -> int:
416+
retry_after_header = headers.get("retry-after")
417+
if not retry_after_header:
418+
raise ValueError("Retry-After header is not present")
419+
420+
try:
421+
parsed_http_date = self.__deserialize_datetime(retry_after_header).replace(
422+
tzinfo=datetime.timezone.utc
423+
)
424+
now = datetime.datetime.now(datetime.timezone.utc)
425+
wait_time_in_sec = (parsed_http_date - now).total_seconds()
426+
except ApiException:
427+
wait_time_in_sec = int(retry_after_header)
428+
429+
if wait_time_in_sec > 1800 or wait_time_in_sec < 1:
430+
raise ValueError("Retry-After header is invalid")
431+
432+
return math.ceil(wait_time_in_sec)
433+
398434
def sanitize_for_serialization(self, obj):
399435
"""Builds a JSON POST object.
400436
@@ -825,7 +861,7 @@ def __deserialize_datetime(self, string):
825861
return parse(string)
826862
except ImportError:
827863
return string
828-
except ValueError:
864+
except (TypeError, ValueError):
829865
raise rest.ApiException(
830866
status=0,
831867
reason=(f"Failed to parse `{string}` as datetime object"),

openfga_sdk/configuration.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ class RetryParams:
4141
4242
:param max_retry: Maximum number of retry
4343
:param min_wait_in_ms: Minimum wait (in ms) between retry
44+
:param max_wait_in_sec: Maximum wait (in seconds) between retry
4445
"""
4546

46-
def __init__(self, max_retry=3, min_wait_in_ms=100):
47+
def __init__(self, max_retry=3, min_wait_in_ms=100, max_wait_in_sec=120):
4748
self._max_retry = max_retry
4849
self._min_wait_in_ms = min_wait_in_ms
50+
self._max_wait_in_sec = max_wait_in_sec
4951

5052
@property
5153
def max_retry(self):
@@ -95,6 +97,25 @@ def min_wait_in_ms(self, value):
9597

9698
self._min_wait_in_ms = value
9799

100+
@property
101+
def max_wait_in_sec(self):
102+
"""
103+
Return the maximum allowed wait (in seconds) in between retry
104+
"""
105+
return self._max_wait_in_sec
106+
107+
@max_wait_in_sec.setter
108+
def max_wait_in_sec(self, value):
109+
"""
110+
Update the maximum allowed wait (in seconds) in between retry
111+
"""
112+
if not isinstance(value, int) or value < 0:
113+
raise FgaValidationException(
114+
"RetryParams.max_wait_in_sec must be an integer greater than or equal to 0"
115+
)
116+
117+
self._max_wait_in_sec = value
118+
98119

99120
class Configuration:
100121
"""NOTE: This class is auto generated by OpenAPI Generator

openfga_sdk/exceptions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@
1010
NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT.
1111
"""
1212

13-
# Specifc FGA header to be parsed
13+
# Specific FGA header to be parsed
1414
X_RATELIMIT_LIMIT = "x-ratelimit-limit"
1515
X_RATELIMIT_REMAINING = "x_ratelimit_remaining"
1616
X_RATELIMIT_RESET = "x_ratelimit_reset"
1717
FGA_REQUEST_ID = "fga-request-id"
1818
FGA_QUERY_DURATION_MS = "fga-query-duration-ms"
1919
OPENFGA_AUTHORIZATION_MODEL_ID = "openfga_authorization_model_id"
20+
RETRY_AFTER = "retry-after"
2021
RESPONSE_HEADERS_TO_KEEP = [
2122
X_RATELIMIT_LIMIT,
2223
X_RATELIMIT_REMAINING,
2324
X_RATELIMIT_RESET,
2425
FGA_REQUEST_ID,
2526
FGA_QUERY_DURATION_MS,
2627
OPENFGA_AUTHORIZATION_MODEL_ID,
28+
RETRY_AFTER,
2729
]
2830

2931

openfga_sdk/sync/api_client.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ def random_time(loop_count, min_wait_in_ms) -> float:
4747
"""
4848
minimum = math.ceil(2**loop_count * min_wait_in_ms)
4949
maximum = math.ceil(2 ** (loop_count + 1) * min_wait_in_ms)
50-
5150
return random.randrange(minimum, maximum) / 1000
5251

5352

@@ -253,11 +252,21 @@ def __call_api(
253252
)
254253
else 0
255254
)
255+
max_wait_in_sec = (
256+
self.configuration.retry_params.max_wait_in_sec
257+
if (
258+
self.configuration.retry_params is not None
259+
and self.configuration.retry_params.max_wait_in_sec is not None
260+
)
261+
else 120
262+
)
256263
if _retry_params is not None:
257264
if _retry_params.max_retry is not None:
258265
max_retry = _retry_params.max_retry
259266
if _retry_params.min_wait_in_ms is not None:
260267
max_retry = _retry_params.min_wait_in_ms
268+
if _retry_params.max_wait_in_sec is not None:
269+
max_wait_in_sec = _retry_params.max_wait_in_sec
261270

262271
_telemetry_attributes = TelemetryAttributes.fromRequest(
263272
user_agent=self.user_agent,
@@ -300,8 +309,14 @@ def __call_api(
300309
configuration=self.configuration.telemetry,
301310
)
302311

303-
time.sleep(random_time(retry, min_wait_in_ms))
312+
try:
313+
wait_time_in_sec = self._parse_retry_after_header(e.header)
314+
except ValueError:
315+
wait_time_in_sec = min(
316+
random_time(retry, min_wait_in_ms), max_wait_in_sec
317+
)
304318

319+
time.sleep(wait_time_in_sec)
305320
continue
306321
e.body = e.body.decode("utf-8")
307322
response_type = response_types_map.get(e.status, None)
@@ -395,6 +410,25 @@ def __call_api(
395410
else:
396411
return (return_data, response_data.status, response_data.headers)
397412

413+
def _parse_retry_after_header(self, headers) -> int:
414+
retry_after_header = headers.get("retry-after")
415+
if not retry_after_header:
416+
raise ValueError("Retry-After header is not present")
417+
418+
try:
419+
parsed_http_date = self.__deserialize_datetime(retry_after_header).replace(
420+
tzinfo=datetime.timezone.utc
421+
)
422+
now = datetime.datetime.now(datetime.timezone.utc)
423+
wait_time_in_sec = (parsed_http_date - now).total_seconds()
424+
except ApiException:
425+
wait_time_in_sec = int(retry_after_header)
426+
427+
if wait_time_in_sec > 1800 or wait_time_in_sec < 1:
428+
raise ValueError("Retry-After header is invalid")
429+
430+
return math.ceil(wait_time_in_sec)
431+
398432
def sanitize_for_serialization(self, obj):
399433
"""Builds a JSON POST object.
400434
@@ -825,7 +859,7 @@ def __deserialize_datetime(self, string):
825859
return parse(string)
826860
except ImportError:
827861
return string
828-
except ValueError:
862+
except (TypeError, ValueError):
829863
raise rest.ApiException(
830864
status=0,
831865
reason=(f"Failed to parse `{string}` as datetime object"),

0 commit comments

Comments
 (0)