Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import math
import sys
import warnings
from contextlib import suppress
Expand Down Expand Up @@ -1467,9 +1468,16 @@ def _get_default_exit_process(self) -> bool:
return True

def _get_remaining_time(self) -> timedelta | None:
"""Get time remaining from the Actor timeout. Returns `None` if not on an Apify platform."""
"""Get time remaining from the Actor timeout, rounded up to whole seconds with minimum value of 1 second.

API treats 0 second timeout as no timeout, the minimum acceptable timeout is 1 second.

Returns `None` if not on an Apify platform.
"""
smallest_possible_api_timeout = 1
if self.is_at_home() and self.configuration.timeout_at:
return max(self.configuration.timeout_at - datetime.now(tz=UTC), timedelta(0))
remaining = self.configuration.timeout_at - datetime.now(tz=UTC)
return timedelta(seconds=max(math.ceil(remaining.total_seconds()), smallest_possible_api_timeout))

self.log.warning(
'Using the `inherit` argument is only possible when the Actor is running on the Apify platform and '
Expand Down
40 changes: 36 additions & 4 deletions tests/unit/actor/test_actor_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, Mock, patch

import pytest

Expand Down Expand Up @@ -316,15 +316,15 @@ async def test_get_remaining_time_warns_when_not_at_home(caplog: pytest.LogCaptu
assert any('inherit' in msg for msg in caplog.messages)


async def test_get_remaining_time_clamps_negative_to_zero() -> None:
"""Test that _get_remaining_time returns timedelta(0) instead of a negative value when timeout is in the past."""
async def test_get_remaining_time_clamps_negative_to_one() -> None:
"""Test that _get_remaining_time returns 1 second instead of a negative value when timeout is in the past."""
async with Actor:
Actor.configuration.is_at_home = True
Actor.configuration.timeout_at = datetime.now(tz=UTC) - timedelta(minutes=5)

result = Actor._get_remaining_time()
assert result is not None
assert result == timedelta(0)
assert result == timedelta(seconds=1)


async def test_get_remaining_time_returns_positive_when_timeout_in_future() -> None:
Expand All @@ -339,6 +339,38 @@ async def test_get_remaining_time_returns_positive_when_timeout_in_future() -> N
assert result <= timedelta(minutes=5)


@pytest.mark.parametrize('method_name', ['start', 'call'])
async def test_actor_start_and_call_skipped_when_no_inherited_time_remains(
apify_client_async_patcher: ApifyClientAsyncPatcher,
method_name: str,
) -> None:
"""Test that Actor.start/Actor.call with `timeout='inherit'` is skipped when the run is past its timeout."""
apify_client_async_patcher.patch('actor', method_name, return_value=Mock())

async with Actor:
Actor.configuration.is_at_home = True
Actor.configuration.timeout_at = datetime.now(tz=UTC) - timedelta(minutes=5)
await getattr(Actor, method_name)('some-actor-id', timeout='inherit')

assert len(apify_client_async_patcher.calls['actor'][method_name]) == 1
assert apify_client_async_patcher.calls['actor'][method_name][0][1]['run_timeout'] == timedelta(seconds=1)


async def test_actor_call_task_skipped_when_no_inherited_time_remains(
apify_client_async_patcher: ApifyClientAsyncPatcher,
) -> None:
"""Test that Actor.call_task with `timeout='inherit'` is skipped when the run is past its timeout."""
apify_client_async_patcher.patch('task', 'call', return_value=Mock())

async with Actor:
Actor.configuration.is_at_home = True
Actor.configuration.timeout_at = datetime.now(tz=UTC) - timedelta(minutes=5)
await Actor.call_task('some-task-id', timeout='inherit')

assert len(apify_client_async_patcher.calls['task']['call']) == 1
assert apify_client_async_patcher.calls['task']['call'][0][1]['run_timeout'] == timedelta(seconds=1)


async def test_reboot_runs_all_listeners_even_when_one_fails(
apify_client_async_patcher: ApifyClientAsyncPatcher,
caplog: pytest.LogCaptureFixture,
Expand Down
Loading