Skip to content

Commit 05662ac

Browse files
committed
feat(DEVC-1752): added requests.Session reuse; replaced third-party tenacity with builtin urllib3 Retry
1 parent 3d3ded9 commit 05662ac

4 files changed

Lines changed: 73 additions & 72 deletions

File tree

src/corva/api.py

Lines changed: 21 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import json
22
import posixpath
33
import re
4-
from http import HTTPStatus
4+
55
from typing import List, Optional, Sequence, Union
66

77
import requests
8-
from tenacity import (
9-
RetryError,
10-
retry,
11-
retry_if_result,
12-
stop_after_attempt,
13-
wait_random_exponential,
14-
)
8+
9+
from corva.api_utils import get_retry_strategy, get_requests_session
10+
from corva.configuration import SETTINGS
1511

1612

1713
class Api:
@@ -21,26 +17,31 @@ class Api:
2117
convenient URL usage and reasonable timeouts to API requests.
2218
"""
2319

24-
TIMEOUT_LIMITS = (3, 30) # seconds
25-
DEFAULT_MAX_RETRIES = int(0)
26-
2720
def __init__(
2821
self,
2922
*,
3023
api_url: str,
3124
data_api_url: str,
3225
api_key: str,
3326
app_key: str,
34-
timeout: Optional[int] = None,
3527
app_connection_id: Optional[int] = None,
28+
max_retries: Optional[int] = SETTINGS.MAX_RETRY_COUNT,
29+
pool_connections_count: Optional[int] = SETTINGS.POOL_CONNECTIONS_COUNT,
30+
pool_max_size: Optional[int] = SETTINGS.POOL_MAX_SIZE,
31+
pool_block: Optional[bool] = SETTINGS.POOL_BLOCK,
3632
):
3733
self.api_url = api_url
3834
self.data_api_url = data_api_url
3935
self.api_key = api_key
4036
self.app_key = app_key
4137
self.app_connection_id = app_connection_id
42-
self.timeout = timeout or self.TIMEOUT_LIMITS[1]
43-
self._max_retries = self.DEFAULT_MAX_RETRIES
38+
self._retry_strategy = get_retry_strategy(max_retries) if max_retries not in (None, 0) else None
39+
self._session = get_requests_session(
40+
retry_strategy=self._retry_strategy,
41+
pool_connections_count=pool_connections_count,
42+
pool_max_size=pool_max_size,
43+
pool_block=pool_block,
44+
)
4445

4546
@property
4647
def default_headers(self):
@@ -99,15 +100,14 @@ def _get_url(self, path: str):
99100

100101
return posixpath.join(self.api_url, path)
101102

102-
@staticmethod
103103
def _execute_request(
104+
self,
104105
method: str,
105106
url: str,
106107
params: Optional[dict],
107108
data: Optional[dict],
108109
headers: Optional[dict] = None,
109-
timeout: Optional[int] = None,
110-
):
110+
) -> requests.Response:
111111
"""Executes the request.
112112
113113
Args:
@@ -116,18 +116,17 @@ def _execute_request(
116116
data: request body, that will be casted to json.
117117
params: url query string params.
118118
headers: additional headers to include in request.
119-
timeout: custom request timeout in seconds.
120119
121120
Returns:
122121
requests.Response instance.
123122
"""
124-
return requests.request(
123+
124+
return self._session.request(
125125
method=method,
126126
url=url,
127127
params=params,
128128
json=data,
129129
headers=headers,
130-
timeout=timeout,
131130
)
132131

133132
def _request(
@@ -138,7 +137,6 @@ def _request(
138137
data: Optional[dict] = None,
139138
params: Optional[dict] = None,
140139
headers: Optional[dict] = None,
141-
timeout: Optional[int] = None,
142140
) -> requests.Response:
143141
"""Prepares HTTP request.
144142
@@ -148,70 +146,24 @@ def _request(
148146
data: request body, that will be casted to json.
149147
params: url query string params.
150148
headers: additional headers to include in request.
151-
timeout: custom request timeout in seconds.
152149
153150
Returns:
154151
requests.Response instance.
155152
"""
156-
retryable_status_codes = [
157-
HTTPStatus.TOO_MANY_REQUESTS, # 428
158-
HTTPStatus.INTERNAL_SERVER_ERROR, # 500
159-
HTTPStatus.BAD_GATEWAY, # 502
160-
HTTPStatus.SERVICE_UNAVAILABLE, # 503
161-
HTTPStatus.GATEWAY_TIMEOUT, # 504
162-
]
163-
164-
timeout = timeout or self.timeout
165-
self._validate_timeout(timeout)
166-
167153
url = self._get_url(path)
168154

169155
headers = {
170156
**self.default_headers,
171157
**(headers or {}),
172158
}
173159

174-
if self.max_retries > 0:
175-
retry_decorator = retry(
176-
stop=stop_after_attempt(self.max_retries),
177-
wait=wait_random_exponential(multiplier=0.25, max=10),
178-
retry=retry_if_result(
179-
lambda r: r.status_code in retryable_status_codes
180-
),
181-
)
182-
retrying_request = retry_decorator(self._execute_request)
183-
try:
184-
response = retrying_request(
185-
method=method,
186-
url=url,
187-
params=params,
188-
data=data,
189-
headers=headers,
190-
timeout=timeout,
191-
)
192-
except RetryError as e:
193-
if not e.last_attempt.failed:
194-
response = e.last_attempt.result()
195-
else:
196-
raise
197-
else:
198-
response = self._execute_request(
160+
return self._execute_request(
199161
method=method,
200162
url=url,
201163
params=params,
202164
data=data,
203165
headers=headers,
204-
timeout=timeout,
205-
)
206-
207-
return response
208-
209-
def _validate_timeout(self, timeout: int) -> None:
210-
if self.TIMEOUT_LIMITS[0] > timeout or self.TIMEOUT_LIMITS[1] < timeout:
211-
raise ValueError(
212-
f"Timeout must be between {self.TIMEOUT_LIMITS[0]} and "
213-
f"{self.TIMEOUT_LIMITS[1]} seconds."
214-
)
166+
)
215167

216168
def get_dataset(
217169
self,

src/corva/api_utils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Optional
2+
3+
import requests
4+
from requests.adapters import HTTPAdapter
5+
from urllib3 import Retry
6+
7+
RETRYABLE_STATUS_CODES = (
8+
429, # HTTPStatus.TOO_MANY_REQUESTS
9+
500, # HTTPStatus.INTERNAL_SERVER_ERROR
10+
502, # HTTPStatus.BAD_GATEWAY
11+
503, # HTTPStatus.SERVICE_UNAVAILABLE
12+
504, # HTTPStatus.GATEWAY_TIMEOUT
13+
)
14+
15+
16+
def get_retry_strategy(max_retries: int) -> Retry:
17+
return Retry(
18+
total=max_retries,
19+
backoff_factor=0.3,
20+
status_forcelist=RETRYABLE_STATUS_CODES,
21+
allowed_methods={'GET', 'POST'}, # Support both
22+
raise_on_status=False,
23+
)
24+
25+
26+
def get_requests_session(
27+
pool_connections_count: int,
28+
pool_max_size: int,
29+
pool_block: bool,
30+
retry_strategy: Optional[Retry] = None,
31+
) -> requests.Session:
32+
adapter = HTTPAdapter(
33+
max_retries=retry_strategy,
34+
pool_connections=pool_connections_count,
35+
pool_maxsize=pool_max_size,
36+
pool_block=pool_block,
37+
)
38+
39+
session = requests.Session()
40+
41+
session.mount('https://', adapter)
42+
session.mount('http://', adapter)
43+
44+
return session

src/corva/configuration.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,13 @@ class Settings(pydantic.BaseSettings):
2323
# secrets
2424
SECRETS_CACHE_TTL: int = int(datetime.timedelta(minutes=5).total_seconds())
2525

26+
# keep-alive
27+
POOL_CONNECTIONS_COUNT: int = 20 # Total pools count
28+
POOL_MAX_SIZE: int = 20 # Max connections count per pool/host
29+
POOL_BLOCK: bool = True # If all conn ack - wait until pool connection released, do not throw exc
30+
31+
# retry
32+
MAX_RETRY_COUNT: int = 3 # If `0` then retires will be disabled
33+
2634

2735
SETTINGS = Settings()

src/corva/handlers.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ def wrapper(
173173
data_api_url=SETTINGS.DATA_API_ROOT_URL,
174174
api_key=api_key,
175175
app_key=SETTINGS.APP_KEY,
176-
timeout=None,
177176
app_connection_id=event.app_connection_id,
178177
)
179178

@@ -282,7 +281,6 @@ def wrapper(
282281
data_api_url=SETTINGS.DATA_API_ROOT_URL,
283282
api_key=api_key,
284283
app_key=SETTINGS.APP_KEY,
285-
timeout=None,
286284
app_connection_id=event.app_connection_id,
287285
)
288286

@@ -392,7 +390,6 @@ def wrapper(
392390
data_api_url=SETTINGS.DATA_API_ROOT_URL,
393391
api_key=api_key,
394392
app_key=SETTINGS.APP_KEY,
395-
timeout=None,
396393
app_connection_id=None,
397394
)
398395

0 commit comments

Comments
 (0)