Skip to content

Commit 79a9ad5

Browse files
committed
fix: correct handled/pending counts when reclaiming a previously handled request
1 parent 73cec62 commit 79a9ad5

3 files changed

Lines changed: 72 additions & 12 deletions

File tree

src/apify/storage_clients/_apify/_request_queue_shared_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ async def reclaim_request(
250250
"""Specific implementation of this method for the RQ shared access mode."""
251251
# Check if the request was marked as handled and clear it. When reclaiming,
252252
# we want to put the request back for processing.
253-
if request.was_already_handled:
253+
# Capture this before clearing `handled_at`, otherwise the computed `was_already_handled` property
254+
# would always be False below and the metadata counters would never be adjusted.
255+
was_already_handled = request.was_already_handled
256+
if was_already_handled:
254257
request.handled_at = None
255258

256259
# Reclaim with lock to prevent race conditions that could lead to double processing of the same request.
@@ -262,7 +265,7 @@ async def reclaim_request(
262265

263266
# If the request was previously handled, decrement our handled count since
264267
# we're putting it back for processing.
265-
if request.was_already_handled and not processed_request.was_already_handled:
268+
if was_already_handled and not processed_request.was_already_handled:
266269
self.metadata.handled_request_count -= 1
267270
self.metadata.pending_request_count += 1
268271

src/apify/storage_clients/_apify/_request_queue_single_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ async def reclaim_request(
250250

251251
request_id = unique_key_to_request_id(request.unique_key)
252252

253-
if request.was_already_handled:
253+
# Capture this before clearing `handled_at`, otherwise the computed `was_already_handled` property
254+
# would always be False below and the metadata counters would never be adjusted.
255+
was_already_handled = request.was_already_handled
256+
if was_already_handled:
254257
request.handled_at = None
255258

256259
try:
@@ -271,7 +274,7 @@ async def reclaim_request(
271274
processed_request.unique_key = request.unique_key
272275
# If the request was previously handled, decrement our handled count since
273276
# we're putting it back for processing.
274-
if request.was_already_handled and not processed_request.was_already_handled:
277+
if was_already_handled and not processed_request.was_already_handled:
275278
self.metadata.handled_request_count -= 1
276279
self.metadata.pending_request_count += 1
277280

tests/unit/storage_clients/test_apify_request_queue_client.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
from __future__ import annotations
22

33
from datetime import UTC, datetime
4+
from typing import TYPE_CHECKING
45
from unittest.mock import AsyncMock
56

67
import pytest
78

89
from apify_client._models import Request as ClientRequest
9-
from apify_client._models import RequestQueueHead
10+
from apify_client._models import RequestQueueHead, RequestRegistration
1011
from crawlee.storage_clients.models import RequestQueueMetadata
1112

13+
from apify import Request
14+
from apify.storage_clients._apify._request_queue_shared_client import ApifyRequestQueueSharedClient
1215
from apify.storage_clients._apify._request_queue_single_client import ApifyRequestQueueSingleClient
1316
from apify.storage_clients._apify._utils import unique_key_to_request_id
1417

18+
if TYPE_CHECKING:
19+
from collections.abc import Callable
1520

16-
def _make_single_client(
17-
api_client: AsyncMock | None = None,
18-
) -> tuple[ApifyRequestQueueSingleClient, AsyncMock]:
19-
if api_client is None:
20-
api_client = AsyncMock()
21+
22+
def _make_metadata() -> RequestQueueMetadata:
2123
now = datetime.now(tz=UTC)
22-
metadata = RequestQueueMetadata(
24+
return RequestQueueMetadata(
2325
id='test-rq-id',
2426
name='test-rq',
2527
accessed_at=now,
@@ -30,7 +32,28 @@ def _make_single_client(
3032
pending_request_count=0,
3133
total_request_count=0,
3234
)
33-
client = ApifyRequestQueueSingleClient(api_client=api_client, metadata=metadata, cache_size=100)
35+
36+
37+
def _make_single_client(
38+
api_client: AsyncMock | None = None,
39+
) -> tuple[ApifyRequestQueueSingleClient, AsyncMock]:
40+
if api_client is None:
41+
api_client = AsyncMock()
42+
client = ApifyRequestQueueSingleClient(api_client=api_client, metadata=_make_metadata(), cache_size=100)
43+
return client, api_client
44+
45+
46+
def _make_shared_client(
47+
api_client: AsyncMock | None = None,
48+
) -> tuple[ApifyRequestQueueSharedClient, AsyncMock]:
49+
if api_client is None:
50+
api_client = AsyncMock()
51+
client = ApifyRequestQueueSharedClient(
52+
api_client=api_client,
53+
metadata=_make_metadata(),
54+
cache_size=100,
55+
metadata_getter=AsyncMock(),
56+
)
3457
return client, api_client
3558

3659

@@ -136,3 +159,34 @@ async def test_fetch_next_request_skips_already_handled() -> None:
136159
assert result is None, 'Already-handled request must not be fetched.'
137160
assert request_id not in client._requests_in_progress, 'Handled request must not be left in progress.'
138161
assert request_id in client._requests_already_handled, 'Handled request id should be cached for deduplication.'
162+
163+
164+
@pytest.mark.parametrize(
165+
'make_client',
166+
[_make_single_client, _make_shared_client],
167+
ids=['single_client', 'shared_client'],
168+
)
169+
async def test_reclaim_previously_handled_adjusts_counts(
170+
make_client: Callable[[], tuple[ApifyRequestQueueSingleClient | ApifyRequestQueueSharedClient, AsyncMock]],
171+
) -> None:
172+
"""Reclaiming a previously handled request must move it from handled back to pending in the metadata."""
173+
client, api_client = make_client()
174+
client.metadata.handled_request_count = 1
175+
client.metadata.pending_request_count = 0
176+
177+
unique_key = 'https://example.com'
178+
request_id = unique_key_to_request_id(unique_key)
179+
request = Request.from_url(unique_key, unique_key=unique_key)
180+
request.handled_at = datetime.now(tz=UTC)
181+
182+
# After reclaiming, the platform reports the request as no longer handled.
183+
api_client.update_request = AsyncMock(
184+
return_value=RequestRegistration.model_validate(
185+
{'requestId': request_id, 'wasAlreadyPresent': True, 'wasAlreadyHandled': False}
186+
)
187+
)
188+
189+
await client.reclaim_request(request)
190+
191+
assert client.metadata.handled_request_count == 0, 'Reclaimed request must be removed from the handled count.'
192+
assert client.metadata.pending_request_count == 1, 'Reclaimed request must be added back to the pending count.'

0 commit comments

Comments
 (0)