Skip to content

Commit 45bdcb6

Browse files
authored
Timeouts and retries (#86)
* added timeout and max retries attributes to ServerAPI * get the values from environment variables * use the timeout and max retries in '_do_rest_request' * fix attribute error of 'RestApiResponse' * added functions to public api * Use empty bytes instead of None * add _ENV_KEY suffix to constants * use private attributes
1 parent d6d6bca commit 45bdcb6

File tree

4 files changed

+180
-28
lines changed

4 files changed

+180
-28
lines changed

ayon_api/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
patch,
4949
delete,
5050

51+
get_timeout,
52+
set_timeout,
53+
get_max_retries,
54+
set_max_retries,
55+
5156
get_event,
5257
get_events,
5358
dispatch_event,
@@ -245,6 +250,11 @@
245250
"patch",
246251
"delete",
247252

253+
"get_timeout",
254+
"set_timeout",
255+
"get_max_retries",
256+
"set_max_retries",
257+
248258
"get_event",
249259
"get_events",
250260
"dispatch_event",

ayon_api/_api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,26 @@ def delete(*args, **kwargs):
474474
return con.delete(*args, **kwargs)
475475

476476

477+
def get_timeout(*args, **kwargs):
478+
con = get_server_api_connection()
479+
return con.get_timeout(*args, **kwargs)
480+
481+
482+
def set_timeout(*args, **kwargs):
483+
con = get_server_api_connection()
484+
return con.set_timeout(*args, **kwargs)
485+
486+
487+
def get_max_retries(*args, **kwargs):
488+
con = get_server_api_connection()
489+
return con.get_max_retries(*args, **kwargs)
490+
491+
492+
def set_max_retries(*args, **kwargs):
493+
con = get_server_api_connection()
494+
return con.set_max_retries(*args, **kwargs)
495+
496+
477497
def get_event(*args, **kwargs):
478498
con = get_server_api_connection()
479499
return con.get_event(*args, **kwargs)

ayon_api/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Environments where server url and api key are stored for global connection
22
SERVER_URL_ENV_KEY = "AYON_SERVER_URL"
33
SERVER_API_ENV_KEY = "AYON_API_KEY"
4+
SERVER_TIMEOUT_ENV_KEY = "AYON_SERVER_TIMEOUT"
5+
SERVER_RETRIES_ENV_KEY = "AYON_SERVER_RETRIES"
6+
47
# Backwards compatibility
58
SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY
69

ayon_api/server_api.py

Lines changed: 147 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import io
44
import json
5+
import time
56
import logging
67
import collections
78
import platform
@@ -26,6 +27,8 @@
2627
from json import JSONDecodeError as RequestsJSONDecodeError
2728

2829
from .constants import (
30+
SERVER_TIMEOUT_ENV_KEY,
31+
SERVER_RETRIES_ENV_KEY,
2932
DEFAULT_PRODUCT_TYPE_FIELDS,
3033
DEFAULT_PROJECT_FIELDS,
3134
DEFAULT_FOLDER_FIELDS,
@@ -127,6 +130,8 @@ def __init__(self, response, data=None):
127130

128131
@property
129132
def text(self):
133+
if self._response is None:
134+
return self.detail
130135
return self._response.text
131136

132137
@property
@@ -135,6 +140,8 @@ def orig_response(self):
135140

136141
@property
137142
def headers(self):
143+
if self._response is None:
144+
return {}
138145
return self._response.headers
139146

140147
@property
@@ -148,6 +155,8 @@ def data(self):
148155

149156
@property
150157
def content(self):
158+
if self._response is None:
159+
return b""
151160
return self._response.content
152161

153162
@property
@@ -339,7 +348,11 @@ class ServerAPI(object):
339348
variable value 'AYON_CERT_FILE' by default.
340349
create_session (Optional[bool]): Create session for connection if
341350
token is available. Default is True.
351+
timeout (Optional[float]): Timeout for requests.
352+
max_retries (Optional[int]): Number of retries for requests.
342353
"""
354+
_default_timeout = 10.0
355+
_default_max_retries = 3
343356

344357
def __init__(
345358
self,
@@ -352,6 +365,8 @@ def __init__(
352365
ssl_verify=None,
353366
cert=None,
354367
create_session=True,
368+
timeout=None,
369+
max_retries=None,
355370
):
356371
if not base_url:
357372
raise ValueError("Invalid server URL {}".format(str(base_url)))
@@ -370,6 +385,9 @@ def __init__(
370385
)
371386
self._sender = sender
372387

388+
self._timeout = timeout
389+
self._max_retries = max_retries
390+
373391
if ssl_verify is None:
374392
# Custom AYON env variable for CA file or 'True'
375393
# - that should cover most default behaviors in 'requests'
@@ -474,6 +492,87 @@ def set_cert(self, cert):
474492
ssl_verify = property(get_ssl_verify, set_ssl_verify)
475493
cert = property(get_cert, set_cert)
476494

495+
@classmethod
496+
def get_default_timeout(cls):
497+
"""Default value for requests timeout.
498+
499+
First looks for environment variable SERVER_TIMEOUT_ENV_KEY which
500+
can affect timeout value. If not available then use class
501+
attribute '_default_timeout'.
502+
503+
Returns:
504+
float: Timeout value in seconds.
505+
"""
506+
507+
try:
508+
return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY))
509+
except (ValueError, TypeError):
510+
pass
511+
512+
return cls._default_timeout
513+
514+
@classmethod
515+
def get_default_max_retries(cls):
516+
"""Default value for requests max retries.
517+
518+
First looks for environment variable SERVER_RETRIES_ENV_KEY, which
519+
can affect max retries value. If not available then use class
520+
attribute '_default_max_retries'.
521+
522+
Returns:
523+
int: Max retries value.
524+
"""
525+
526+
try:
527+
return int(os.environ.get(SERVER_RETRIES_ENV_KEY))
528+
except (ValueError, TypeError):
529+
pass
530+
531+
return cls._default_max_retries
532+
533+
def get_timeout(self):
534+
"""Current value for requests timeout.
535+
536+
Returns:
537+
float: Timeout value in seconds.
538+
"""
539+
540+
return self._timeout
541+
542+
def set_timeout(self, timeout):
543+
"""Change timeout value for requests.
544+
545+
Args:
546+
timeout (Union[float, None]): Timeout value in seconds.
547+
"""
548+
549+
if timeout is None:
550+
timeout = self.get_default_timeout()
551+
self._timeout = float(timeout)
552+
553+
def get_max_retries(self):
554+
"""Current value for requests max retries.
555+
556+
Returns:
557+
int: Max retries value.
558+
"""
559+
560+
return self._max_retries
561+
562+
def set_max_retries(self, max_retries):
563+
"""Change max retries value for requests.
564+
565+
Args:
566+
max_retries (Union[int, None]): Max retries value.
567+
"""
568+
569+
if max_retries is None:
570+
max_retries = self.get_default_max_retries()
571+
self._max_retries = int(max_retries)
572+
573+
timeout = property(get_timeout, set_timeout)
574+
max_retries = property(get_max_retries, set_max_retries)
575+
477576
@property
478577
def access_token(self):
479578
"""Access token used for authorization to server.
@@ -1004,6 +1103,10 @@ def _logout(self):
10041103
logout_from_server(self._base_url, self._access_token)
10051104

10061105
def _do_rest_request(self, function, url, **kwargs):
1106+
kwargs.setdefault("timeout", self.timeout)
1107+
max_retries = kwargs.get("max_retries", self.max_retries)
1108+
if max_retries < 1:
1109+
max_retries = 1
10071110
if self._session is None:
10081111
# Validate token if was not yet validated
10091112
# - ignore validation if we're in middle of
@@ -1023,38 +1126,54 @@ def _do_rest_request(self, function, url, **kwargs):
10231126
elif isinstance(function, RequestType):
10241127
function = self._session_functions_mapping[function]
10251128

1026-
try:
1027-
response = function(url, **kwargs)
1129+
response = None
1130+
new_response = None
1131+
for _ in range(max_retries):
1132+
try:
1133+
response = function(url, **kwargs)
1134+
break
1135+
1136+
except ConnectionRefusedError:
1137+
# Server may be restarting
1138+
new_response = RestApiResponse(
1139+
None,
1140+
{"detail": "Unable to connect the server. Connection refused"}
1141+
)
1142+
except requests.exceptions.Timeout:
1143+
# Connection timed out
1144+
new_response = RestApiResponse(
1145+
None,
1146+
{"detail": "Connection timed out."}
1147+
)
1148+
except requests.exceptions.ConnectionError:
1149+
# Other connection error (ssl, etc) - does not make sense to
1150+
# try call server again
1151+
new_response = RestApiResponse(
1152+
None,
1153+
{"detail": "Unable to connect the server. Connection error"}
1154+
)
1155+
break
10281156

1029-
except ConnectionRefusedError:
1030-
new_response = RestApiResponse(
1031-
None,
1032-
{"detail": "Unable to connect the server. Connection refused"}
1033-
)
1034-
except requests.exceptions.ConnectionError:
1035-
new_response = RestApiResponse(
1036-
None,
1037-
{"detail": "Unable to connect the server. Connection error"}
1038-
)
1039-
else:
1040-
content_type = response.headers.get("Content-Type")
1041-
if content_type == "application/json":
1042-
try:
1043-
new_response = RestApiResponse(response)
1044-
except JSONDecodeError:
1045-
new_response = RestApiResponse(
1046-
None,
1047-
{
1048-
"detail": "The response is not a JSON: {}".format(
1049-
response.text)
1050-
}
1051-
)
1157+
time.sleep(0.1)
10521158

1053-
elif content_type in ("image/jpeg", "image/png"):
1054-
new_response = RestApiResponse(response)
1159+
if new_response is not None:
1160+
return new_response
10551161

1056-
else:
1162+
content_type = response.headers.get("Content-Type")
1163+
if content_type == "application/json":
1164+
try:
10571165
new_response = RestApiResponse(response)
1166+
except JSONDecodeError:
1167+
new_response = RestApiResponse(
1168+
None,
1169+
{
1170+
"detail": "The response is not a JSON: {}".format(
1171+
response.text)
1172+
}
1173+
)
1174+
1175+
else:
1176+
new_response = RestApiResponse(response)
10581177

10591178
self.log.debug("Response {}".format(str(new_response)))
10601179
return new_response

0 commit comments

Comments
 (0)