Skip to content

Commit e7cd6b4

Browse files
committed
fix: cache post-update handled state when marking and reclaiming requests
1 parent 87af5b8 commit e7cd6b4

2 files changed

Lines changed: 77 additions & 6 deletions

File tree

src/apify/storage_clients/_apify/_request_queue_shared_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,6 @@ async def mark_request_as_handled(self, request: Request) -> ProcessedRequest |
264264
if request.handled_at is None:
265265
request.handled_at = datetime.now(tz=UTC)
266266

267-
if cached_request := self._requests_cache.get(request_id):
268-
cached_request.was_already_handled = request.was_already_handled
269267
try:
270268
# Update the request in the API
271269
processed_request = await self._update_request(request)
@@ -277,10 +275,11 @@ async def mark_request_as_handled(self, request: Request) -> ProcessedRequest |
277275
self.metadata.handled_request_count += 1
278276
self.metadata.pending_request_count -= 1
279277

280-
# Update the cache with the handled request
278+
# Cache the request as handled. The platform response's `was_already_handled` reports the state
279+
# before this update, so it must not be cached as the request's current state.
281280
self._cache_request(
282281
cache_key=request_id,
283-
processed_request=processed_request,
282+
processed_request=processed_request.model_copy(update={'was_already_handled': True}),
284283
hydrated_request=request,
285284
)
286285
except Exception:
@@ -314,11 +313,12 @@ async def reclaim_request(
314313
self.metadata.handled_request_count -= 1
315314
self.metadata.pending_request_count += 1
316315

317-
# Update the cache
316+
# Cache the request as pending again. The platform response's `was_already_handled` reports the
317+
# state before this update, so it must not be cached as the request's current state.
318318
request_id = unique_key_to_request_id(request.unique_key)
319319
self._cache_request(
320320
request_id,
321-
processed_request,
321+
processed_request.model_copy(update={'was_already_handled': False}),
322322
hydrated_request=request,
323323
)
324324

tests/unit/storage_clients/test_apify_request_queue_client.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
RequestDraft,
1616
RequestQueueHead,
1717
RequestQueueStats,
18+
RequestRegistration,
1819
)
1920
from apify_client._models import Request as ClientRequest
2021
from crawlee.storage_clients.models import AddRequestsResponse, RequestQueueMetadata
@@ -120,6 +121,15 @@ def _client_request(request: Request, *, handled_at: datetime | None) -> ClientR
120121
)
121122

122123

124+
def _request_registration(request: Request, *, was_already_handled: bool) -> RequestRegistration:
125+
"""Build an `update_request` response reporting the given pre-update handled state."""
126+
return RequestRegistration(
127+
request_id=unique_key_to_request_id(request.unique_key),
128+
was_already_present=True,
129+
was_already_handled=was_already_handled,
130+
)
131+
132+
123133
def test_unique_key_to_request_id_length() -> None:
124134
unique_key = 'exampleKey123'
125135
request_id = unique_key_to_request_id(unique_key, request_id_length=15)
@@ -431,6 +441,67 @@ async def test_shared_is_finished_true_on_queue_with_no_known_requests() -> None
431441
api_client.get_request.assert_not_awaited()
432442

433443

444+
async def test_shared_is_finished_true_after_this_client_marked_request_handled() -> None:
445+
"""A request this client marked handled is trusted from the cache, so `is_finished` needs no per-request read."""
446+
client, api_client = _make_shared_client()
447+
request = Request.from_url('https://example.com/1')
448+
449+
api_client.batch_add_requests = AsyncMock(return_value=_batch_result_all_processed([request]))
450+
await client.add_batch_of_requests([request])
451+
452+
# The platform reports the pre-update state, so a first-time handle comes back as not yet handled.
453+
api_client.update_request = AsyncMock(return_value=_request_registration(request, was_already_handled=False))
454+
assert await client.mark_request_as_handled(request) is not None
455+
456+
api_client.list_and_lock_head = AsyncMock(return_value=_empty_locked_head())
457+
api_client.get_request = AsyncMock()
458+
459+
assert await client.is_finished() is True
460+
api_client.get_request.assert_not_awaited()
461+
462+
463+
async def test_shared_is_finished_false_after_failed_mark_request_as_handled() -> None:
464+
"""A failed `mark_request_as_handled` leaves the request unconfirmed, so `is_finished` re-checks it."""
465+
client, api_client = _make_shared_client()
466+
request = Request.from_url('https://example.com/1')
467+
request_id = unique_key_to_request_id(request.unique_key)
468+
469+
api_client.batch_add_requests = AsyncMock(return_value=_batch_result_all_processed([request]))
470+
await client.add_batch_of_requests([request])
471+
472+
api_client.update_request = AsyncMock(side_effect=RuntimeError('network down'))
473+
assert await client.mark_request_as_handled(request) is None
474+
475+
api_client.list_and_lock_head = AsyncMock(return_value=_empty_locked_head())
476+
api_client.get_request = AsyncMock(return_value=_client_request(request, handled_at=None))
477+
478+
assert await client.is_finished() is False
479+
api_client.get_request.assert_awaited_once_with(request_id)
480+
481+
482+
async def test_shared_is_finished_false_after_reclaiming_handled_request() -> None:
483+
"""A reclaimed previously-handled request is pending again, so `is_finished` re-checks it via the platform."""
484+
client, api_client = _make_shared_client()
485+
request = Request.from_url('https://example.com/1')
486+
request_id = unique_key_to_request_id(request.unique_key)
487+
488+
api_client.batch_add_requests = AsyncMock(return_value=_batch_result_all_processed([request]))
489+
await client.add_batch_of_requests([request])
490+
491+
api_client.update_request = AsyncMock(return_value=_request_registration(request, was_already_handled=False))
492+
await client.mark_request_as_handled(request)
493+
494+
# Reclaim the handled request: the platform reports the pre-update (handled) state.
495+
api_client.update_request = AsyncMock(return_value=_request_registration(request, was_already_handled=True))
496+
assert await client.reclaim_request(request) is not None
497+
498+
api_client.list_and_lock_head = AsyncMock(return_value=_empty_locked_head())
499+
api_client.get_request = AsyncMock(return_value=_client_request(request, handled_at=None))
500+
501+
assert await client.is_finished() is False
502+
api_client.get_request.assert_awaited_once_with(request_id)
503+
504+
434505
async def test_shared_is_finished_false_while_add_batch_in_flight() -> None:
435506
"""The queue does not report finished while an `add_batch_of_requests` call is still in flight."""
436507
client, api_client = _make_shared_client()

0 commit comments

Comments
 (0)