Skip to content

Commit 69b8af9

Browse files
vdusekclaude
andauthored
fix: Clamp negative timedelta in _get_remaining_time() (#818)
## Summary `_get_remaining_time()` could return a negative `timedelta` if the Actor's timeout had already passed. This clamps the result to `timedelta(0)` so callers never receive a negative remaining time. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bd56901 commit 69b8af9

File tree

2 files changed

+25
-2
lines changed

2 files changed

+25
-2
lines changed

src/apify/_actor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1398,7 +1398,7 @@ def _get_default_exit_process(self) -> bool:
13981398
def _get_remaining_time(self) -> timedelta | None:
13991399
"""Get time remaining from the Actor timeout. Returns `None` if not on an Apify platform."""
14001400
if self.is_at_home() and self.configuration.timeout_at:
1401-
return self.configuration.timeout_at - datetime.now(tz=timezone.utc)
1401+
return max(self.configuration.timeout_at - datetime.now(tz=timezone.utc), timedelta(0))
14021402

14031403
self.log.warning(
14041404
'Using `inherit` or `RemainingTime` argument is only possible when the Actor'

tests/unit/actor/test_actor_helpers.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import asyncio
44
import warnings
5-
from datetime import timedelta
5+
from datetime import datetime, timedelta, timezone
66
from typing import TYPE_CHECKING
77

88
import pytest
@@ -321,3 +321,26 @@ async def test_get_remaining_time_warns_when_not_at_home(caplog: pytest.LogCaptu
321321
result = Actor._get_remaining_time()
322322
assert result is None
323323
assert any('inherit' in msg or 'RemainingTime' in msg for msg in caplog.messages)
324+
325+
326+
async def test_get_remaining_time_clamps_negative_to_zero() -> None:
327+
"""Test that _get_remaining_time returns timedelta(0) instead of a negative value when timeout is in the past."""
328+
async with Actor:
329+
Actor.configuration.is_at_home = True
330+
Actor.configuration.timeout_at = datetime.now(tz=timezone.utc) - timedelta(minutes=5)
331+
332+
result = Actor._get_remaining_time()
333+
assert result is not None
334+
assert result == timedelta(0)
335+
336+
337+
async def test_get_remaining_time_returns_positive_when_timeout_in_future() -> None:
338+
"""Test that _get_remaining_time returns a positive timedelta when timeout is in the future."""
339+
async with Actor:
340+
Actor.configuration.is_at_home = True
341+
Actor.configuration.timeout_at = datetime.now(tz=timezone.utc) + timedelta(minutes=5)
342+
343+
result = Actor._get_remaining_time()
344+
assert result is not None
345+
assert result > timedelta(0)
346+
assert result <= timedelta(minutes=5)

0 commit comments

Comments
 (0)