Skip to content

chore: Defensively discard in-progress id on unexpected None from platform#875

Merged
vdusek merged 2 commits intomasterfrom
fix/bug-011-fetch-next-request-leak
Apr 21, 2026
Merged

chore: Defensively discard in-progress id on unexpected None from platform#875
vdusek merged 2 commits intomasterfrom
fix/bug-011-fetch-next-request-leak

Conversation

@vdusek
Copy link
Copy Markdown
Contributor

@vdusek vdusek commented Apr 20, 2026

Summary

Defensive guard in ApifyRequestQueueSingleClient.fetch_next_request: if _get_request_by_id unexpectedly returns None for an id just surfaced by _list_head, discard the id from _requests_in_progress before continuing to the next head entry. Without this guard, the id would be permanently stuck in _requests_in_progress — poisoning is_empty() forever and filtering the id out of every subsequent head reconciliation, with no public recovery path (the caller receives None, so has no Request to reclaim).

Found by code reading — no known real-world trigger under single-producer/single-consumer operation. Kept as a boundary check against unexpected platform responses.

Test plan

  • Existing unit tests pass

🤖 Generated with Claude Code

`ApifyRequestQueueSingleClient.fetch_next_request` committed the id to
`_requests_in_progress` before awaiting `_get_request_by_id`. When the
platform's `get_request` returns `None` (e.g. propagation delay after a
fresh `_list_head`), the method returned `None` to the caller but the id
stayed in `_requests_in_progress` forever — permanently poisoning
`is_empty()` and excluding the id from future `_list_head` reconciliation,
with no public recovery path (the caller had no `Request` to reclaim).

Discard the id on the `None` path and continue to the next head entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added this to the 139th sprint - Tooling team milestone Apr 20, 2026
@github-actions github-actions Bot added t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics. labels Apr 20, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.94%. Comparing base (737b0dd) to head (afde14a).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
...age_clients/_apify/_request_queue_single_client.py 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #875      +/-   ##
==========================================
+ Coverage   86.57%   86.94%   +0.36%     
==========================================
  Files          48       48              
  Lines        2920     2926       +6     
==========================================
+ Hits         2528     2544      +16     
+ Misses        392      382      -10     
Flag Coverage Δ
e2e 37.83% <0.00%> (-0.05%) ⬇️
integration 59.26% <60.00%> (-0.02%) ⬇️
unit 75.63% <0.00%> (+0.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vdusek vdusek added the adhoc Ad-hoc unplanned task added during the sprint. label Apr 20, 2026
@vdusek vdusek requested a review from Pijukatel April 20, 2026 18:32
client, api_client = _make_single_client()
api_client.list_head = AsyncMock(return_value={'items': [], 'hadMultipleClients': False})
api_client.get_request = AsyncMock(return_value=None)
client._head_requests.append('missing-id')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is modifying the internals to establish a state that might not even be possible. Or can we achieve this state without touching the internals?

Normally, something can get into _head_requests from two sources:

  • Request added by this client, and then it will be in the local cache so it will not return None
  • It was already present on the platform, so it will also not return None, unless someone deleted it in the meantime (which is out of scope of this single client)

Is it some edge case that can happen around the cache limit? But if it was recently added, it will still be in cache; it will be removed from cache only if it was too old and in that case the data propagation delay is long over.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is modifying the internals to establish a state that might not even be possible. Or can we achieve this state without touching the internals?

Probably with some patches, I'll give it a try.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I wasn't able to reproduce this. Claude was probably hallucinating around the single/shared clients. I've kept just the guard as a defensive approach against unexpected platform responses, and updated the code and PR title accordingly. Check it out.

Comment thread src/apify/storage_clients/_apify/_request_queue_single_client.py Outdated
@vdusek vdusek changed the title fix: discard in-progress id when request queue get_request returns None chore(request-queue): defensively discard in-progress id on unexpected None from platform Apr 21, 2026
@vdusek vdusek changed the title chore(request-queue): defensively discard in-progress id on unexpected None from platform chore: Defensively discard in-progress id on unexpected None from platform Apr 21, 2026
@vdusek vdusek requested a review from Pijukatel April 21, 2026 10:08
@vdusek vdusek merged commit b017cb9 into master Apr 21, 2026
31 checks passed
@vdusek vdusek deleted the fix/bug-011-fetch-next-request-leak branch April 21, 2026 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants