-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathtest_client_timeouts.py
More file actions
218 lines (172 loc) · 9.01 KB
/
test_client_timeouts.py
File metadata and controls
218 lines (172 loc) · 9.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from unittest.mock import Mock
import pytest
from impit import HTTPError, Response, TimeoutException
from apify_client import ApifyClient
from apify_client._http_client import HTTPClient, HTTPClientAsync
from apify_client.client import DEFAULT_TIMEOUT
from apify_client.clients import DatasetClient, KeyValueStoreClient, RequestQueueClient
from apify_client.clients.resource_clients import dataset, request_queue
from apify_client.clients.resource_clients import key_value_store as kvs
if TYPE_CHECKING:
from collections.abc import Iterator
from pytest_httpserver import HTTPServer
class EndOfTestError(Exception):
"""Custom exception that is raised after the relevant part of the code is executed to stop the test."""
@pytest.fixture
def patch_request(monkeypatch: pytest.MonkeyPatch) -> Iterator[list]:
timeouts = []
def mock_request(*_args: Any, **kwargs: Any) -> None:
timeouts.append(kwargs.get('timeout'))
raise EndOfTestError
async def mock_request_async(*args: Any, **kwargs: Any) -> None:
return mock_request(*args, **kwargs)
monkeypatch.setattr('impit.Client.request', mock_request)
monkeypatch.setattr('impit.AsyncClient.request', mock_request_async)
yield timeouts
monkeypatch.undo()
async def test_dynamic_timeout_async_client(monkeypatch: pytest.MonkeyPatch) -> None:
"""Tests timeout values for request with retriable errors.
Values should increase with each attempt, starting from initial call value and bounded by the client timeout value.
"""
should_raise_error = iter((True, True, True, False))
call_timeout = 1
client_timeout = 5
expected_timeouts = [call_timeout, 2, 4, client_timeout]
retry_counter_mock = Mock()
timeouts = []
async def mock_request(*_args: Any, **kwargs: Any) -> Response:
timeouts.append(kwargs.get('timeout'))
retry_counter_mock()
should_raise = next(should_raise_error)
if should_raise:
raise TimeoutException
return Response(status_code=200)
monkeypatch.setattr('impit.AsyncClient.request', mock_request)
response = await HTTPClientAsync(timeout_secs=client_timeout).call(
method='GET', url='http://placeholder.url/async_timeout', timeout_secs=call_timeout
)
# Check that the retry counter was called the expected number of times
# (4 times: 3 retries + 1 final successful call)
assert retry_counter_mock.call_count == 4
assert timeouts == expected_timeouts
# Check that the response is successful
assert response.status_code == 200
async def test_retry_on_http_error_async_client(monkeypatch: pytest.MonkeyPatch) -> None:
"""Tests that bare impit.HTTPError (e.g. body decode errors) are retried.
This reproduces the scenario where the HTTP response body is truncated mid-stream
(e.g. "unexpected EOF during chunk size line"), which impit raises as a generic HTTPError.
"""
should_raise_error = iter((True, True, False))
retry_counter_mock = Mock()
async def mock_request(*_args: Any, **_kwargs: Any) -> Response:
retry_counter_mock()
should_raise = next(should_raise_error)
if should_raise:
raise HTTPError('The internal HTTP library has thrown an error: unexpected EOF during chunk size line')
return Response(status_code=200)
monkeypatch.setattr('impit.AsyncClient.request', mock_request)
response = await HTTPClientAsync(timeout_secs=5).call(method='GET', url='http://placeholder.url/http_error')
# 3 attempts: 2 failures + 1 success
assert retry_counter_mock.call_count == 3
assert response.status_code == 200
def test_dynamic_timeout_sync_client(monkeypatch: pytest.MonkeyPatch) -> None:
"""Tests timeout values for request with retriable errors.
Values should increase with each attempt, starting from initial call value and bounded by the client timeout value.
"""
should_raise_error = iter((True, True, True, False))
call_timeout = 1
client_timeout = 5
expected_timeouts = [call_timeout, 2, 4, client_timeout]
retry_counter_mock = Mock()
timeouts = []
def mock_request(*_args: Any, **kwargs: Any) -> Response:
timeouts.append(kwargs.get('timeout'))
retry_counter_mock()
should_raise = next(should_raise_error)
if should_raise:
raise TimeoutException
return Response(status_code=200)
monkeypatch.setattr('impit.Client.request', mock_request)
response = HTTPClient(timeout_secs=client_timeout).call(
method='GET', url='http://placeholder.url/sync_timeout', timeout_secs=call_timeout
)
# Check that the retry counter was called the expected number of times
# (4 times: 3 retries + 1 final successful call)
assert retry_counter_mock.call_count == 4
assert timeouts == expected_timeouts
# Check that the response is successful
assert response.status_code == 200
_timeout_params = [
(DatasetClient, 'get', dataset._SMALL_TIMEOUT, {}),
(DatasetClient, 'update', dataset._SMALL_TIMEOUT, {}),
(DatasetClient, 'delete', dataset._SMALL_TIMEOUT, {}),
(DatasetClient, 'list_items', DEFAULT_TIMEOUT, {}),
(DatasetClient, 'download_items', DEFAULT_TIMEOUT, {}),
(DatasetClient, 'get_items_as_bytes', DEFAULT_TIMEOUT, {}),
(DatasetClient, 'push_items', dataset._MEDIUM_TIMEOUT, {'items': {}}),
(DatasetClient, 'get_statistics', dataset._SMALL_TIMEOUT, {}),
(KeyValueStoreClient, 'get', kvs._SMALL_TIMEOUT, {}),
(KeyValueStoreClient, 'update', DEFAULT_TIMEOUT, {}),
(KeyValueStoreClient, 'delete', kvs._SMALL_TIMEOUT, {}),
(KeyValueStoreClient, 'list_keys', kvs._MEDIUM_TIMEOUT, {}),
(KeyValueStoreClient, 'get_record', DEFAULT_TIMEOUT, {'key': 'some_key'}),
(KeyValueStoreClient, 'get_record_as_bytes', DEFAULT_TIMEOUT, {'key': 'some_key'}),
(KeyValueStoreClient, 'set_record', DEFAULT_TIMEOUT, {'key': 'some_key', 'value': 'some_value'}),
(KeyValueStoreClient, 'delete_record', kvs._SMALL_TIMEOUT, {'key': 'some_key'}),
(RequestQueueClient, 'get', request_queue._SMALL_TIMEOUT, {}),
(RequestQueueClient, 'update', request_queue._SMALL_TIMEOUT, {}),
(RequestQueueClient, 'delete', request_queue._SMALL_TIMEOUT, {}),
(RequestQueueClient, 'list_head', request_queue._SMALL_TIMEOUT, {}),
(RequestQueueClient, 'list_and_lock_head', request_queue._MEDIUM_TIMEOUT, {'lock_secs': 1}),
(RequestQueueClient, 'add_request', request_queue._SMALL_TIMEOUT, {'request': {}}),
(RequestQueueClient, 'get_request', request_queue._SMALL_TIMEOUT, {'request_id': 'some_id'}),
(RequestQueueClient, 'update_request', request_queue._MEDIUM_TIMEOUT, {'request': {'id': 123}}),
(RequestQueueClient, 'delete_request', request_queue._SMALL_TIMEOUT, {'request_id': 123}),
(RequestQueueClient, 'prolong_request_lock', request_queue._MEDIUM_TIMEOUT, {'request_id': 123, 'lock_secs': 1}),
(RequestQueueClient, 'delete_request_lock', request_queue._SMALL_TIMEOUT, {'request_id': 123}),
(RequestQueueClient, 'batch_add_requests', request_queue._MEDIUM_TIMEOUT, {'requests': [{}]}),
(RequestQueueClient, 'batch_delete_requests', request_queue._SMALL_TIMEOUT, {'requests': [{}]}),
(RequestQueueClient, 'list_requests', request_queue._MEDIUM_TIMEOUT, {}),
]
# This test will probably need to be reworked or skipped when switching to `impit`.
# Without the mock library, it's difficult to reproduce, maybe with monkeypatch?
@pytest.mark.parametrize(
('client_type', 'method', 'expected_timeout', 'kwargs'),
_timeout_params,
)
def test_specific_timeouts_for_specific_endpoints_sync(
client_type: type[DatasetClient | KeyValueStoreClient | RequestQueueClient],
method: str,
kwargs: dict,
expected_timeout: int,
patch_request: list[float | None],
httpserver: HTTPServer,
) -> None:
httpserver.expect_request('/').respond_with_data(status=200)
client = client_type(base_url=httpserver.url_for('/'), root_client=ApifyClient(), http_client=HTTPClient())
with pytest.raises(EndOfTestError):
getattr(client, method)(**kwargs)
assert len(patch_request) == 1
assert patch_request[0] == expected_timeout
# This test will probably need to be reworked or skipped when switching to `impit`.
# Without the mock library, it's difficult to reproduce, maybe with monkeypatch?
@pytest.mark.parametrize(
('client_type', 'method', 'expected_timeout', 'kwargs'),
_timeout_params,
)
async def test_specific_timeouts_for_specific_endpoints_async(
client_type: type[DatasetClient | KeyValueStoreClient | RequestQueueClient],
method: str,
kwargs: dict,
expected_timeout: int,
patch_request: list[float | None],
httpserver: HTTPServer,
) -> None:
httpserver.expect_request('/').respond_with_data(status=200)
client = client_type(base_url=httpserver.url_for('/'), root_client=ApifyClient(), http_client=HTTPClient())
with pytest.raises(EndOfTestError):
await getattr(client, method)(**kwargs)
assert len(patch_request) == 1
assert patch_request[0] == expected_timeout