Skip to content

Commit 717ca9a

Browse files
✨ Implement retry strategy (#135)
* update workflow before publishing python package * fix dependency issue and bump version * point to website in project description * fix broken dependency * improve doc * add github token to download artifacts * ensure only read-access @wvangeit * yet another attempt at downloading artifacts * make sure to use repo that ran the trigger wf * another attempt at fixing * change owner * allow publishing to testpypi also when pr * minor change * revert minor (but breaking) change * minor fix * add debug messages * another debug message * hopefully the final version * final fix * minor fix * move master and tag to individual jobs * add debug messages * dev->post * add python script for determining semantic version * minor changes * minor changes * improve error handling and add version file to artifacts * check if release * minor fix * ensure to enter venv * also when tagging * source venv in publishin workflow * ensure only master * add script for testing 'pure' semver * adapt workflows to new python script * minor change * attempt to evaluate expressions correctly * several fixes to fix tests * ensure repo is checked out in publish workflow * several small fixes * cleanup * debug * minor cleanup * mionr changes * add debug message * minor change * minor change * yet another try * minor change * minor change * minor change * mionr change * minor changes * correct workflow run id * cosmetic change * avoid using gh * change to a single job for publishing * minor cleanup * swap loops in clean up jobs * correction * add retry * improve type hints * improve retry strategy @pcrespov * py 3.7 compatible * py 3.7 compatible * another attempt * minor fix
1 parent 3fce0b4 commit 717ca9a

File tree

4 files changed

+137
-8
lines changed

4 files changed

+137
-8
lines changed

clients/python/client/osparc/_files_api.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any, Iterator, List, Optional, Tuple, Union
99

1010
import httpx
11-
from httpx import AsyncClient, Response
11+
from httpx import Response
1212
from osparc_client import (
1313
BodyCompleteMultipartUploadV0FilesFileIdCompletePost,
1414
ClientFile,
@@ -114,7 +114,9 @@ async def upload_file_async(
114114

115115
uploaded_parts: list[UploadedPart] = []
116116
print("- uploading chunks...")
117-
async with AsyncHttpClient(timeout=timeout_seconds) as session:
117+
async with AsyncHttpClient(
118+
configuration=self.api_client.configuration, timeout=timeout_seconds
119+
) as session:
118120
async for chunck, size in tqdm(
119121
file_chunk_generator(file, chunk_size), total=n_urls
120122
):
@@ -130,6 +132,7 @@ async def upload_file_async(
130132
)
131133

132134
async with AsyncHttpClient(
135+
configuration=self.api_client.configuration,
133136
request_type="post",
134137
url=links.abort_upload,
135138
base_url=self.api_client.configuration.host,
@@ -148,7 +151,7 @@ async def upload_file_async(
148151

149152
async def _complete_multipart_upload(
150153
self,
151-
http_client: AsyncClient,
154+
http_client: AsyncHttpClient,
152155
complete_link: str,
153156
client_file: ClientFile,
154157
uploaded_parts: List[UploadedPart],
@@ -167,7 +170,7 @@ async def _complete_multipart_upload(
167170

168171
async def _upload_chunck(
169172
self,
170-
http_client: AsyncClient,
173+
http_client: AsyncHttpClient,
171174
chunck: bytes,
172175
chunck_size: int,
173176
upload_link: str,

clients/python/client/osparc/_http_client.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
from typing import Optional
1+
from datetime import datetime
2+
from email.utils import parsedate_to_datetime
3+
from typing import Any, Awaitable, Callable, Optional, Set
24

35
import httpx
46
import tenacity
7+
from osparc_client import Configuration
8+
9+
_RETRY_AFTER_STATUS_CODES: Set[int] = {429, 503}
510

611

712
class AsyncHttpClient:
@@ -10,16 +15,18 @@ class AsyncHttpClient:
1015
def __init__(
1116
self,
1217
*,
18+
configuration: Configuration,
1319
request_type: Optional[str] = None,
1420
url: Optional[str] = None,
15-
**httpx_async_client_kwargs
21+
**httpx_async_client_kwargs,
1622
):
23+
self.configuration = configuration
1724
self._client = httpx.AsyncClient(**httpx_async_client_kwargs)
1825
self._callback = getattr(self._client, request_type) if request_type else None
1926
self._url = url
2027

21-
async def __aenter__(self) -> httpx.AsyncClient:
22-
return self._client
28+
async def __aenter__(self) -> "AsyncHttpClient":
29+
return self
2330

2431
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
2532
if exc_value is None:
@@ -41,3 +48,57 @@ async def __aexit__(self, exc_type, exc_value, traceback) -> None:
4148
raise err from exc_value
4249
await self._client.aclose()
4350
raise exc_value
51+
52+
async def _request(
53+
self, method: Callable[[Any], Awaitable[httpx.Response]], *args, **kwargs
54+
) -> httpx.Response:
55+
n_attempts = self.configuration.retries.total
56+
assert isinstance(n_attempts, int)
57+
58+
@tenacity.retry(
59+
reraise=True,
60+
wait=self._wait_callback,
61+
stop=tenacity.stop_after_attempt(n_attempts),
62+
retry=tenacity.retry_if_exception_type(httpx.HTTPStatusError),
63+
)
64+
async def _():
65+
response: httpx.Response = await method(*args, **kwargs)
66+
if response.status_code in self.configuration.retries.status_forcelist:
67+
response.raise_for_status()
68+
return response
69+
70+
return await _()
71+
72+
async def put(self, *args, **kwargs) -> httpx.Response:
73+
return await self._request(self._client.put, *args, **kwargs)
74+
75+
async def post(self, *args, **kwargs) -> httpx.Response:
76+
return await self._request(self._client.post, *args, **kwargs)
77+
78+
async def delete(self, *args, **kwargs) -> httpx.Response:
79+
return await self._request(self._client.delete, *args, **kwargs)
80+
81+
async def patch(self, *args, **kwargs) -> httpx.Response:
82+
return await self._request(self._client.patch, *args, **kwargs)
83+
84+
def _wait_callback(self, retry_state: tenacity.RetryCallState) -> int:
85+
assert retry_state.outcome is not None
86+
response: httpx.Response = retry_state.outcome.exception().response
87+
if response.status_code in _RETRY_AFTER_STATUS_CODES:
88+
retry_after = response.headers.get("Retry-After")
89+
if retry_after is not None:
90+
try:
91+
next_try = parsedate_to_datetime(retry_after)
92+
return int(
93+
(next_try - datetime.now(tz=next_try.tzinfo)).total_seconds()
94+
)
95+
except (ValueError, TypeError):
96+
pass
97+
try:
98+
return int(retry_after)
99+
except ValueError:
100+
pass
101+
# https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html#utilities
102+
return self.configuration.retries.backoff_factor * (
103+
2**retry_state.attempt_number
104+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Iterator
2+
3+
import osparc
4+
import pytest
5+
6+
7+
@pytest.fixture
8+
def cfg() -> Iterator[osparc.Configuration]:
9+
yield osparc.Configuration(
10+
host="https://1a52d8d7-9f9f-48e5-b2f0-a226e6b25f0b.com",
11+
username="askjdg",
12+
password="asdjbaskjdb",
13+
)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import httpx
2+
import osparc
3+
import pytest
4+
from osparc._http_client import AsyncHttpClient
5+
6+
7+
@pytest.fixture
8+
def fake_retry_state():
9+
def _(r: httpx.Response):
10+
class Exc:
11+
response = r
12+
13+
class Outcome:
14+
def exception(self):
15+
return Exc()
16+
17+
class FakeRetryCallState:
18+
outcome = Outcome()
19+
20+
return FakeRetryCallState()
21+
22+
yield _
23+
24+
25+
def test_retry_strategy(cfg: osparc.Configuration, fake_retry_state):
26+
async_client = AsyncHttpClient(
27+
configuration=cfg,
28+
request_type="get",
29+
url="79ae41cc-0d89-4714-ac9d-c23ee1b110ce",
30+
)
31+
assert (
32+
async_client._wait_callback(
33+
fake_retry_state(
34+
httpx.Response(
35+
status_code=503,
36+
headers={"Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT"},
37+
)
38+
)
39+
)
40+
< 0
41+
)
42+
assert (
43+
async_client._wait_callback(
44+
fake_retry_state(
45+
httpx.Response(
46+
status_code=503,
47+
headers={"Retry-After": "15"},
48+
)
49+
)
50+
)
51+
== 15
52+
)

0 commit comments

Comments
 (0)