Skip to content

Commit fefa36b

Browse files
feat(python): Test python timeout (generated)
algolia/api-clients-automation#5883 Co-authored-by: algolia-bot <accounts+algolia-api-client-bot@algolia.com> Co-authored-by: Eric Zaharia <94015633+eric-zaharia@users.noreply.github.com>
1 parent 4d3e8bd commit fefa36b

File tree

2 files changed

+217
-24
lines changed

2 files changed

+217
-24
lines changed

algoliasearch/http/transporter.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from typing import List, Optional
44

55
from aiohttp import ClientSession, ClientTimeout, TCPConnector
6-
from async_timeout import timeout
76

87
from algoliasearch.http.api_response import ApiResponse
98
from algoliasearch.http.base_config import BaseConfig
@@ -60,33 +59,32 @@ async def request(
6059
request_options.timeouts["connect"] * (host.retry_count + 1)
6160
) / 1000
6261
request_timeout = self._timeout / 1000
63-
total_timeout = connect_timeout + request_timeout
6462

65-
async with timeout(total_timeout):
66-
timeout_config = ClientTimeout(connect=connect_timeout)
67-
68-
resp = await self._session.request(
69-
method=verb,
70-
url=url,
71-
headers=request_options.headers,
72-
data=request_options.data,
73-
proxy=proxy,
74-
timeout=timeout_config,
75-
)
63+
timeout_config = ClientTimeout(
64+
connect=connect_timeout, sock_read=request_timeout
65+
)
7666

67+
async with self._session.request(
68+
method=verb,
69+
url=url,
70+
headers=request_options.headers,
71+
data=request_options.data,
72+
proxy=proxy,
73+
timeout=timeout_config,
74+
) as resp:
7775
_raw_data = await resp.text()
7876

79-
response = ApiResponse(
80-
verb=verb,
81-
path=path,
82-
url=url,
83-
host=host.url,
84-
status_code=resp.status,
85-
headers=resp.headers, # pyright: ignore # insensitive dict is still a dict
86-
data=_raw_data,
87-
raw_data=_raw_data,
88-
error_message=str(resp.reason),
89-
)
77+
response = ApiResponse(
78+
verb=verb,
79+
path=path,
80+
url=url,
81+
host=host.url,
82+
status_code=resp.status,
83+
headers=resp.headers, # pyright: ignore # insensitive dict is still a dict
84+
data=_raw_data,
85+
raw_data=_raw_data,
86+
error_message=str(resp.reason),
87+
)
9088

9189
except TimeoutError as e:
9290
response = ApiResponse(
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import asyncio
2+
import time
3+
from os import environ
4+
from typing import Tuple
5+
6+
from algoliasearch.http.base_config import BaseConfig
7+
from algoliasearch.http.hosts import CallType, Host, HostsCollection
8+
from algoliasearch.http.request_options import RequestOptions
9+
from algoliasearch.http.transporter import Transporter
10+
from algoliasearch.http.transporter_sync import TransporterSync
11+
from algoliasearch.http.verb import Verb
12+
13+
TEST_SERVER = (
14+
"localhost" if environ.get("CI") == "true" else "host.docker.internal"
15+
) + ":6676"
16+
17+
18+
def create_config_with_host(host_url: str) -> Tuple[BaseConfig, Host]:
19+
config = BaseConfig("test-app", "test-key")
20+
host = Host(host_url, accept=CallType.READ | CallType.WRITE)
21+
config.hosts = HostsCollection([host])
22+
return config, host
23+
24+
25+
def create_server_host() -> Host:
26+
return Host(TEST_SERVER, scheme="http", accept=CallType.READ | CallType.WRITE)
27+
28+
29+
def test_sync_retry_count_stateful():
30+
"""connect timeout increases across failed requests: 2s -> 4s -> 6s."""
31+
config, _ = create_config_with_host("10.255.255.1")
32+
transporter = TransporterSync(config)
33+
request_options = RequestOptions(config).merge()
34+
35+
times = []
36+
for _ in range(3):
37+
start = time.time()
38+
try:
39+
transporter.request(
40+
verb=Verb.GET,
41+
path="/test",
42+
request_options=request_options,
43+
use_read_transporter=True,
44+
)
45+
except Exception:
46+
times.append(time.time() - start)
47+
48+
assert 1.5 < times[0] < 2.5, f"Request 1 should be ~2s, got {times[0]:.2f}s"
49+
assert 3.5 < times[1] < 4.5, f"Request 2 should be ~4s, got {times[1]:.2f}s"
50+
assert 5.5 < times[2] < 7.0, f"Request 3 should be ~6s, got {times[2]:.2f}s"
51+
52+
53+
def test_sync_retry_count_resets():
54+
"""retry_count resets to 0 after successful request."""
55+
config, bad_host = create_config_with_host("10.255.255.1")
56+
good_host = create_server_host()
57+
58+
transporter = TransporterSync(config)
59+
request_options = RequestOptions(config).merge()
60+
61+
# fail twice to increment retry_count
62+
for _ in range(2):
63+
try:
64+
transporter.request(
65+
verb=Verb.GET,
66+
path="/test",
67+
request_options=request_options,
68+
use_read_transporter=True,
69+
)
70+
except Exception:
71+
pass
72+
73+
# switch to good host and succeed
74+
config.hosts = HostsCollection([good_host])
75+
transporter._hosts = [good_host]
76+
good_host.retry_count = bad_host.retry_count
77+
78+
response = transporter.request(
79+
verb=Verb.GET,
80+
path="/1/test/instant",
81+
request_options=request_options,
82+
use_read_transporter=True,
83+
)
84+
assert response.status_code == 200
85+
assert good_host.retry_count == 0, (
86+
f"retry_count should reset to 0, got {good_host.retry_count}"
87+
)
88+
89+
# point to bad host again, should timeout at 2s (not 6s)
90+
good_host.url = "10.255.255.1"
91+
good_host.scheme = "https"
92+
93+
start = time.time()
94+
try:
95+
transporter.request(
96+
verb=Verb.GET,
97+
path="/test",
98+
request_options=request_options,
99+
use_read_transporter=True,
100+
)
101+
assert False, "Request should have timed out"
102+
except Exception:
103+
elapsed = time.time() - start
104+
assert 1.5 < elapsed < 2.5, f"After reset should be ~2s, got {elapsed:.2f}s"
105+
106+
107+
async def test_async_retry_count_stateful():
108+
"""async connect timeout increases across failed requests: 2s -> 4s -> 6s."""
109+
config, _ = create_config_with_host("10.255.255.1")
110+
transporter = Transporter(config)
111+
request_options = RequestOptions(config).merge()
112+
113+
times = []
114+
for _ in range(3):
115+
start = time.time()
116+
try:
117+
await transporter.request(
118+
verb=Verb.GET,
119+
path="/test",
120+
request_options=request_options,
121+
use_read_transporter=True,
122+
)
123+
except Exception:
124+
times.append(time.time() - start)
125+
126+
await transporter.close()
127+
128+
assert 1.5 < times[0] < 2.5, f"Request 1 should be ~2s, got {times[0]:.2f}s"
129+
assert 3.5 < times[1] < 4.5, f"Request 2 should be ~4s, got {times[1]:.2f}s"
130+
assert 5.5 < times[2] < 7.0, f"Request 3 should be ~6s, got {times[2]:.2f}s"
131+
132+
133+
async def test_async_retry_count_resets():
134+
"""async retry_count resets to 0 after successful request."""
135+
config, bad_host = create_config_with_host("10.255.255.1")
136+
good_host = create_server_host()
137+
138+
transporter = Transporter(config)
139+
request_options = RequestOptions(config).merge()
140+
141+
# fail twice to increment retry_count
142+
for _ in range(2):
143+
try:
144+
await transporter.request(
145+
verb=Verb.GET,
146+
path="/test",
147+
request_options=request_options,
148+
use_read_transporter=True,
149+
)
150+
except Exception:
151+
pass
152+
153+
# switch to good host and succeed
154+
config.hosts = HostsCollection([good_host])
155+
transporter._hosts = [good_host]
156+
good_host.retry_count = bad_host.retry_count
157+
158+
response = await transporter.request(
159+
verb=Verb.GET,
160+
path="/1/test/instant",
161+
request_options=request_options,
162+
use_read_transporter=True,
163+
)
164+
assert response.status_code == 200
165+
assert good_host.retry_count == 0, (
166+
f"retry_count should reset to 0, got {good_host.retry_count}"
167+
)
168+
169+
# point to bad host again, should timeout at 2s (not 6s)
170+
good_host.url = "10.255.255.1"
171+
good_host.scheme = "https"
172+
173+
start = time.time()
174+
try:
175+
await transporter.request(
176+
verb=Verb.GET,
177+
path="/test",
178+
request_options=request_options,
179+
use_read_transporter=True,
180+
)
181+
assert False, "Request should have timed out"
182+
except Exception:
183+
elapsed = time.time() - start
184+
assert 1.5 < elapsed < 2.5, f"After reset should be ~2s, got {elapsed:.2f}s"
185+
finally:
186+
await transporter.close()
187+
188+
189+
# pytest integration for async tests
190+
def test_async_retry_count_stateful_sync():
191+
asyncio.run(test_async_retry_count_stateful())
192+
193+
194+
def test_async_retry_count_resets_sync():
195+
asyncio.run(test_async_retry_count_resets())

0 commit comments

Comments
 (0)