Skip to content

Commit fd0663e

Browse files
vdusekclaude
andauthored
fix: correct deadline logic in _wait_for_finish (#749)
## Summary Fixes two bugs in `ResourceClient._wait_for_finish` / `ResourceClientAsync._wait_for_finish`: 1. **`not_found_deadline` anchored at function entry.** The 3-second "job not found" grace window was computed once at entry and never reset. A single transient 404 (e.g. replica lag) during a long poll could terminate the wait and return `None` even though the job was live. The deadline is now lazy — it starts on the first observed 404 and resets after any successful response, so each run of 404s gets its own fresh window. 2. **User `wait_duration` not checked on 404.** When `wait_duration` was shorter than `DEFAULT_WAIT_WHEN_JOB_NOT_EXIST` (3s) and the API returned persistent 404s, the loop could overrun the documented contract by up to ~3s. The 404 branch now checks the user deadline first. Applied symmetrically to the sync and async mirrors. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1dd70b8 commit fd0663e

1 file changed

Lines changed: 22 additions & 12 deletions

File tree

src/apify_client/_resource_clients/_resource_client.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,8 @@ def _wait_for_finish(
308308
Raises:
309309
ApifyApiError: If API returns errors other than 404.
310310
"""
311-
now = datetime.now(UTC)
312-
deadline = (now + wait_duration) if wait_duration is not None else None
313-
not_found_deadline = now + DEFAULT_WAIT_WHEN_JOB_NOT_EXIST
311+
deadline = (datetime.now(UTC) + wait_duration) if wait_duration is not None else None
312+
not_found_deadline: datetime | None = None
314313
actor_job: dict = {}
315314

316315
while True:
@@ -331,6 +330,9 @@ def _wait_for_finish(
331330
actor_job_response = ActorJobResponse.model_validate(result)
332331
actor_job = actor_job_response.data.model_dump()
333332

333+
# Reset the not-found streak so a later transient 404 gets its own grace window.
334+
not_found_deadline = None
335+
334336
is_terminal = actor_job_response.data.status in TERMINAL_STATUSES
335337
is_timed_out = deadline is not None and datetime.now(UTC) >= deadline
336338

@@ -340,9 +342,12 @@ def _wait_for_finish(
340342
except ApifyApiError as exc:
341343
catch_not_found_or_throw(exc)
342344

343-
# If there are still not found errors after DEFAULT_WAIT_WHEN_JOB_NOT_EXIST, we give up
344-
# and return None. In such case, the requested record probably really doesn't exist.
345-
if datetime.now(UTC) > not_found_deadline:
345+
now = datetime.now(UTC)
346+
if deadline is not None and now >= deadline:
347+
return None
348+
if not_found_deadline is None:
349+
not_found_deadline = now + DEFAULT_WAIT_WHEN_JOB_NOT_EXIST
350+
elif now > not_found_deadline:
346351
return None
347352

348353
# It might take some time for database replicas to get up-to-date so sleep a bit before retrying
@@ -496,9 +501,8 @@ async def _wait_for_finish(
496501
Raises:
497502
ApifyApiError: If API returns errors other than 404.
498503
"""
499-
now = datetime.now(UTC)
500-
deadline = (now + wait_duration) if wait_duration is not None else None
501-
not_found_deadline = now + DEFAULT_WAIT_WHEN_JOB_NOT_EXIST
504+
deadline = (datetime.now(UTC) + wait_duration) if wait_duration is not None else None
505+
not_found_deadline: datetime | None = None
502506
actor_job: dict = {}
503507

504508
while True:
@@ -519,6 +523,9 @@ async def _wait_for_finish(
519523
actor_job_response = ActorJobResponse.model_validate(result)
520524
actor_job = actor_job_response.data.model_dump()
521525

526+
# Reset the not-found streak so a later transient 404 gets its own grace window.
527+
not_found_deadline = None
528+
522529
is_terminal = actor_job_response.data.status in TERMINAL_STATUSES
523530
is_timed_out = deadline is not None and datetime.now(UTC) >= deadline
524531

@@ -528,9 +535,12 @@ async def _wait_for_finish(
528535
except ApifyApiError as exc:
529536
catch_not_found_or_throw(exc)
530537

531-
# If there are still not found errors after DEFAULT_WAIT_WHEN_JOB_NOT_EXIST, we give up
532-
# and return None. In such case, the requested record probably really doesn't exist.
533-
if datetime.now(UTC) > not_found_deadline:
538+
now = datetime.now(UTC)
539+
if deadline is not None and now >= deadline:
540+
return None
541+
if not_found_deadline is None:
542+
not_found_deadline = now + DEFAULT_WAIT_WHEN_JOB_NOT_EXIST
543+
elif now > not_found_deadline:
534544
return None
535545

536546
# It might take some time for database replicas to get up-to-date so sleep a bit before retrying

0 commit comments

Comments
 (0)