Skip to content

Commit 879c72d

Browse files
committed
Timeout bug fix
1 parent 1e655c7 commit 879c72d

3 files changed

Lines changed: 232 additions & 7 deletions

File tree

src/apify_client/_resource_clients/actor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ async def start(
841841
maxTotalChargeUsd=max_total_charge_usd,
842842
restartOnError=restart_on_error,
843843
memory=memory_mbytes,
844-
timeout_secs=timeout_secs,
844+
timeout=timeout_secs,
845845
waitForFinish=wait_for_finish,
846846
forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None,
847847
webhooks=encode_webhook_list_to_base64(webhooks) if webhooks is not None else None,
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from typing import TYPE_CHECKING
5+
6+
import pytest
7+
from werkzeug import Request, Response
8+
9+
from apify_client import ApifyClient, ApifyClientAsync
10+
from apify_client._models import ActorJobStatus
11+
12+
if TYPE_CHECKING:
13+
from pytest_httpserver import HTTPServer
14+
15+
_MOCKED_ACTOR_ID = 'test_actor_id'
16+
_MOCKED_RUN_ID = 'test_run_id'
17+
18+
19+
def _create_minimal_run_response() -> dict:
20+
"""Create minimal valid Run response for testing."""
21+
return {
22+
'data': {
23+
'id': _MOCKED_RUN_ID,
24+
'actId': _MOCKED_ACTOR_ID,
25+
'userId': 'test_user_id',
26+
'startedAt': '2019-11-30T07:34:24.202Z',
27+
'finishedAt': '2019-12-12T09:30:12.202Z',
28+
'status': ActorJobStatus.RUNNING.value,
29+
'statusMessage': 'Running',
30+
'isStatusMessageTerminal': False,
31+
'meta': {'origin': 'WEB'},
32+
'stats': {
33+
'restartCount': 0,
34+
'resurrectCount': 0,
35+
'computeUnits': 0.1,
36+
},
37+
'options': {
38+
'build': 'latest',
39+
'timeoutSecs': 300,
40+
'memoryMbytes': 1024,
41+
'diskMbytes': 2048,
42+
},
43+
'buildId': 'test_build_id',
44+
'generalAccess': 'RESTRICTED',
45+
'defaultKeyValueStoreId': 'test_kvs_id',
46+
'defaultDatasetId': 'test_dataset_id',
47+
'defaultRequestQueueId': 'test_rq_id',
48+
'buildNumber': '0.0.1',
49+
'containerUrl': 'https://test.runs.apify.net',
50+
}
51+
}
52+
53+
54+
def test_actor_start_passes_timeout_param_sync(httpserver: HTTPServer) -> None:
55+
"""Test that sync ActorClient.start() passes timeout as 'timeout' query parameter."""
56+
captured_requests: list[Request] = []
57+
58+
def capture_request(request: Request) -> Response:
59+
captured_requests.append(request)
60+
return Response(
61+
response=json.dumps(_create_minimal_run_response()),
62+
status=200,
63+
mimetype='application/json',
64+
)
65+
66+
httpserver.expect_request(
67+
f'/v2/acts/{_MOCKED_ACTOR_ID}/runs',
68+
method='POST',
69+
).respond_with_handler(capture_request)
70+
71+
api_url = httpserver.url_for('/').removesuffix('/')
72+
client = ApifyClient(token='test_token', api_url=api_url)
73+
74+
# Call start with timeout_secs
75+
client.actor(_MOCKED_ACTOR_ID).start(timeout_secs=300)
76+
77+
# Verify the request was made with correct timeout parameter
78+
assert len(captured_requests) == 1
79+
request = captured_requests[0]
80+
81+
# The timeout should be passed as 'timeout' query parameter, not 'timeout_secs'
82+
assert 'timeout' in request.args, "Expected 'timeout' query parameter to be present"
83+
assert request.args['timeout'] == '300', f'Expected timeout=300, got timeout={request.args.get("timeout")}'
84+
assert 'timeout_secs' not in request.args, "Unexpected 'timeout_secs' query parameter"
85+
86+
87+
async def test_actor_start_passes_timeout_param_async(httpserver: HTTPServer) -> None:
88+
"""Test that async ActorClientAsync.start() passes timeout as 'timeout' query parameter."""
89+
captured_requests: list[Request] = []
90+
91+
def capture_request(request: Request) -> Response:
92+
captured_requests.append(request)
93+
return Response(
94+
response=json.dumps(_create_minimal_run_response()),
95+
status=200,
96+
mimetype='application/json',
97+
)
98+
99+
httpserver.expect_request(
100+
f'/v2/acts/{_MOCKED_ACTOR_ID}/runs',
101+
method='POST',
102+
).respond_with_handler(capture_request)
103+
104+
api_url = httpserver.url_for('/').removesuffix('/')
105+
client = ApifyClientAsync(token='test_token', api_url=api_url)
106+
107+
# Call start with timeout_secs
108+
await client.actor(_MOCKED_ACTOR_ID).start(timeout_secs=300)
109+
110+
# Verify the request was made with correct timeout parameter
111+
assert len(captured_requests) == 1
112+
request = captured_requests[0]
113+
114+
# The timeout should be passed as 'timeout' query parameter, not 'timeout_secs'
115+
assert 'timeout' in request.args, "Expected 'timeout' query parameter to be present"
116+
assert request.args['timeout'] == '300', f'Expected timeout=300, got timeout={request.args.get("timeout")}'
117+
assert 'timeout_secs' not in request.args, "Unexpected 'timeout_secs' query parameter"
118+
119+
120+
def test_actor_start_timeout_not_passed_when_none_sync(httpserver: HTTPServer) -> None:
121+
"""Test that sync ActorClient.start() does not pass timeout when not specified."""
122+
captured_requests: list[Request] = []
123+
124+
def capture_request(request: Request) -> Response:
125+
captured_requests.append(request)
126+
return Response(
127+
response=json.dumps(_create_minimal_run_response()),
128+
status=200,
129+
mimetype='application/json',
130+
)
131+
132+
httpserver.expect_request(
133+
f'/v2/acts/{_MOCKED_ACTOR_ID}/runs',
134+
method='POST',
135+
).respond_with_handler(capture_request)
136+
137+
api_url = httpserver.url_for('/').removesuffix('/')
138+
client = ApifyClient(token='test_token', api_url=api_url)
139+
140+
# Call start without timeout_secs
141+
client.actor(_MOCKED_ACTOR_ID).start()
142+
143+
# Verify timeout parameter is not present
144+
assert len(captured_requests) == 1
145+
request = captured_requests[0]
146+
assert 'timeout' not in request.args, "Unexpected 'timeout' query parameter when not specified"
147+
assert 'timeout_secs' not in request.args, "Unexpected 'timeout_secs' query parameter"
148+
149+
150+
async def test_actor_start_timeout_not_passed_when_none_async(httpserver: HTTPServer) -> None:
151+
"""Test that async ActorClientAsync.start() does not pass timeout when not specified."""
152+
captured_requests: list[Request] = []
153+
154+
def capture_request(request: Request) -> Response:
155+
captured_requests.append(request)
156+
return Response(
157+
response=json.dumps(_create_minimal_run_response()),
158+
status=200,
159+
mimetype='application/json',
160+
)
161+
162+
httpserver.expect_request(
163+
f'/v2/acts/{_MOCKED_ACTOR_ID}/runs',
164+
method='POST',
165+
).respond_with_handler(capture_request)
166+
167+
api_url = httpserver.url_for('/').removesuffix('/')
168+
client = ApifyClientAsync(token='test_token', api_url=api_url)
169+
170+
# Call start without timeout_secs
171+
await client.actor(_MOCKED_ACTOR_ID).start()
172+
173+
# Verify timeout parameter is not present
174+
assert len(captured_requests) == 1
175+
request = captured_requests[0]
176+
assert 'timeout' not in request.args, "Unexpected 'timeout' query parameter when not specified"
177+
assert 'timeout_secs' not in request.args, "Unexpected 'timeout_secs' query parameter"
178+
179+
180+
@pytest.mark.parametrize('timeout_value', [60, 300, 3600, 86400])
181+
def test_actor_start_various_timeout_values_sync(httpserver: HTTPServer, timeout_value: int) -> None:
182+
"""Test that various timeout values are correctly passed in sync start()."""
183+
captured_requests: list[Request] = []
184+
185+
def capture_request(request: Request) -> Response:
186+
captured_requests.append(request)
187+
return Response(
188+
response=json.dumps(_create_minimal_run_response()),
189+
status=200,
190+
mimetype='application/json',
191+
)
192+
193+
httpserver.expect_request(
194+
f'/v2/acts/{_MOCKED_ACTOR_ID}/runs',
195+
method='POST',
196+
).respond_with_handler(capture_request)
197+
198+
api_url = httpserver.url_for('/').removesuffix('/')
199+
client = ApifyClient(token='test_token', api_url=api_url)
200+
201+
client.actor(_MOCKED_ACTOR_ID).start(timeout_secs=timeout_value)
202+
203+
assert len(captured_requests) == 1
204+
assert captured_requests[0].args['timeout'] == str(timeout_value)
205+
206+
207+
@pytest.mark.parametrize('timeout_value', [60, 300, 3600, 86400])
208+
async def test_actor_start_various_timeout_values_async(httpserver: HTTPServer, timeout_value: int) -> None:
209+
"""Test that various timeout values are correctly passed in async start()."""
210+
captured_requests: list[Request] = []
211+
212+
def capture_request(request: Request) -> Response:
213+
captured_requests.append(request)
214+
return Response(
215+
response=json.dumps(_create_minimal_run_response()),
216+
status=200,
217+
mimetype='application/json',
218+
)
219+
220+
httpserver.expect_request(
221+
f'/v2/acts/{_MOCKED_ACTOR_ID}/runs',
222+
method='POST',
223+
).respond_with_handler(capture_request)
224+
225+
api_url = httpserver.url_for('/').removesuffix('/')
226+
client = ApifyClientAsync(token='test_token', api_url=api_url)
227+
228+
await client.actor(_MOCKED_ACTOR_ID).start(timeout_secs=timeout_value)
229+
230+
assert len(captured_requests) == 1
231+
assert captured_requests[0].args['timeout'] == str(timeout_value)

tests/unit/test_url_generation.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
"""Unit tests for URL generation logic.
2-
3-
Tests public URL generation for datasets and key-value stores using mocks.
4-
These tests verify URL construction with various API URL configurations.
5-
"""
6-
71
from __future__ import annotations
82

93
import json

0 commit comments

Comments
 (0)