Skip to content

Commit 74de324

Browse files
committed
Draft
1 parent b832919 commit 74de324

File tree

4 files changed

+112
-59
lines changed

4 files changed

+112
-59
lines changed

src/apify/_actor.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import sys
66
from contextlib import suppress
7-
from datetime import timedelta
7+
from datetime import datetime, timedelta, timezone
88
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, cast, overload
99

1010
from lazy_object_proxy import Proxy
@@ -734,9 +734,11 @@ async def start(
734734
serialized_webhooks = None
735735

736736
if timeout == 'RemainingTime':
737-
actor_start_timeout = await self._get_remaining_time(client)
737+
actor_start_timeout = await self._get_remaining_time()
738738
elif isinstance(timeout, str):
739-
raise ValueError(f'`timeout` can be `None`, `RemainingTime` or `timedelta` instance, but is {timeout=}')
739+
raise ValueError(
740+
f'`timeout` can be `None`, `RemainingTime` literal or `timedelta` instance, but is {timeout=}'
741+
)
740742
else:
741743
actor_start_timeout = timeout
742744

@@ -752,14 +754,10 @@ async def start(
752754

753755
return ActorRun.model_validate(api_result)
754756

755-
async def _get_remaining_time(self, client: ApifyClientAsync) -> timedelta | None:
756-
"""Get time remaining from the actor timeout. Returns `None` if not on Apify platform."""
757-
if self.is_at_home() and self.configuration.actor_run_id:
758-
run_data = await client.run(self.configuration.actor_run_id).get()
759-
if run_data is not None and (timeout := run_data.get('options', {}).get('timeoutSecs', None)):
760-
runtime = timedelta(seconds=run_data.get('runTimeSecs', None))
761-
remaining_time = timeout - runtime
762-
return timedelta(seconds=remaining_time)
757+
async def _get_remaining_time(self) -> timedelta | None:
758+
"""Get time remaining from the actor timeout. Returns `None` if not on an Apify platform."""
759+
if self.is_at_home() and self.configuration.timeout_at:
760+
return self.configuration.timeout_at - datetime.now(tz=timezone.utc)
763761

764762
self.log.warning('Using `RemainingTime` argument for timeout outside of the Apify platform. Returning `None`')
765763
return None
@@ -848,9 +846,11 @@ async def call(
848846
serialized_webhooks = None
849847

850848
if timeout == 'RemainingTime':
851-
actor_call_timeout = await self._get_remaining_time(client)
849+
actor_call_timeout = await self._get_remaining_time()
852850
elif isinstance(timeout, str):
853-
raise ValueError(f'`timeout` can be `None`, `RemainingTime` or `timedelta` instance, but is {timeout=}')
851+
raise ValueError(
852+
f'`timeout` can be `None`, `RemainingTime` literal or `timedelta` instance, but is {timeout=}'
853+
)
854854
else:
855855
actor_call_timeout = timeout
856856

tests/integration/conftest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
_TOKEN_ENV_VAR = 'APIFY_TEST_USER_API_TOKEN'
3131
_API_URL_ENV_VAR = 'APIFY_INTEGRATION_TESTS_API_URL'
3232
_SDK_ROOT_PATH = Path(__file__).parent.parent.parent.resolve()
33-
_DEFAULT_TEST_TIMEOUT = 600
33+
3434

3535
@pytest.fixture
3636
def prepare_test_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callable[[], None]:
@@ -315,7 +315,7 @@ async def _make_actor(
315315
name=actor_name,
316316
default_run_build='latest',
317317
default_run_memory_mbytes=256,
318-
default_run_timeout_secs=_DEFAULT_TEST_TIMEOUT,
318+
default_run_timeout_secs=600,
319319
versions=[
320320
{
321321
'versionNumber': '0.0',
@@ -331,7 +331,7 @@ async def _make_actor(
331331
print(f'Building Actor {actor_name}...')
332332
build_result = await actor_client.build(version_number='0.0')
333333
build_client = client.build(build_result['id'])
334-
build_client_result = await build_client.wait_for_finish(wait_secs=_DEFAULT_TEST_TIMEOUT)
334+
build_client_result = await build_client.wait_for_finish(wait_secs=600)
335335

336336
assert build_client_result is not None
337337
assert build_client_result['status'] == ActorJobStatus.SUCCEEDED
@@ -408,7 +408,7 @@ async def _run_actor(
408408

409409
client = ApifyClientAsync(token=apify_token, api_url=os.getenv(_API_URL_ENV_VAR))
410410
run_client = client.run(call_result['id'])
411-
run_result = await run_client.wait_for_finish(wait_secs=_DEFAULT_TEST_TIMEOUT)
411+
run_result = await run_client.wait_for_finish(wait_secs=600)
412412

413413
return ActorRun.model_validate(run_result)
414414

tests/integration/test_actor_call.py

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from typing import TYPE_CHECKING
5+
6+
from apify import Actor
7+
8+
if TYPE_CHECKING:
9+
from .conftest import MakeActorFunction, RunActorFunction
10+
11+
12+
async def test_actor_start_remaining_timeout(
13+
make_actor: MakeActorFunction,
14+
run_actor: RunActorFunction,
15+
) -> None:
16+
async def main() -> None:
17+
from datetime import datetime, timezone
18+
19+
async with Actor:
20+
actor_input = (await Actor.get_input()) or {}
21+
if actor_input.get('called_from_another_actor', False) is True:
22+
return
23+
24+
await asyncio.sleep(1)
25+
# Start another run of this actor with timeout set to the time remaining in this actor run
26+
other_run_data = await Actor.start(
27+
actor_id=Actor.configuration.actor_id or '',
28+
run_input={'called_from_another_actor': True},
29+
timeout='RemainingTime',
30+
)
31+
32+
# To make sure that the actor is started
33+
await asyncio.sleep(5)
34+
35+
assert Actor.configuration.timeout_at is not None
36+
assert Actor.configuration.started_at is not None
37+
assert other_run_data is not None
38+
assert other_run_data.options is not None
39+
40+
remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=timezone.utc)
41+
42+
try:
43+
assert other_run_data.options.timeout > remaining_time_after_actor_start
44+
assert other_run_data.options.timeout < Actor.configuration.timeout_at - Actor.configuration.started_at
45+
finally:
46+
# Abort the other actor run after asserting the timeouts
47+
await Actor.apify_client.run(other_run_data.id).abort()
48+
49+
actor = await make_actor(label='remaining-timeout', main_func=main)
50+
run_result = await run_actor(actor)
51+
52+
assert run_result.status == 'SUCCEEDED'
53+
54+
55+
async def test_actor_call_remaining_timeout(
56+
make_actor: MakeActorFunction,
57+
run_actor: RunActorFunction,
58+
) -> None:
59+
async def main() -> None:
60+
from datetime import datetime, timezone
61+
62+
async with Actor:
63+
actor_input = (await Actor.get_input()) or {}
64+
if actor_input.get('called_from_another_actor', False) is True:
65+
return
66+
67+
await asyncio.sleep(1)
68+
# Start another run of this actor with timeout set to the time remaining in this actor run
69+
other_run_data = await Actor.call(
70+
actor_id=Actor.configuration.actor_id or '',
71+
run_input={'called_from_another_actor': True},
72+
timeout='RemainingTime',
73+
)
74+
75+
# To make sure that the actor is started
76+
await asyncio.sleep(5)
77+
78+
assert Actor.configuration.timeout_at is not None
79+
assert Actor.configuration.started_at is not None
80+
assert other_run_data is not None
81+
assert other_run_data.options is not None
82+
83+
remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=timezone.utc)
84+
85+
try:
86+
assert other_run_data.options.timeout > remaining_time_after_actor_start
87+
assert other_run_data.options.timeout < Actor.configuration.timeout_at - Actor.configuration.started_at
88+
finally:
89+
# Abort the other actor run after asserting the timeouts
90+
await Actor.apify_client.run(other_run_data.id).abort()
91+
92+
actor = await make_actor(label='remaining-timeout', main_func=main)
93+
run_result = await run_actor(actor)
94+
95+
assert run_result.status == 'SUCCEEDED'

0 commit comments

Comments
 (0)