Skip to content

fix(memory-storage): stop re-adds from duplicating and resetting pending requests - #2074

Merged
vdusek merged 11 commits into
apify:masterfrom
anxkhn:perf/memory-rq-forefront-reposition
Jul 29, 2026
Merged

fix(memory-storage): stop re-adds from duplicating and resetting pending requests#2074
vdusek merged 11 commits into
apify:masterfrom
anxkhn:perf/memory-rq-forefront-reposition

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
  • Store pending requests in an OrderedDict keyed by unique key instead of a deque. Repositioning a request to the forefront is now O(1) instead of an O(n) deque.remove() scan, so re-adding K already-pending requests is O(K) instead of O(K*N) — measured 22.4s → 0.004s for 2000 forefront re-adds into a 20k-request queue. The scan only triggered for requests already waiting in the queue, which is what add_requests(..., forefront=True) hits when a handler re-discovers URLs that are still pending.
  • Fix the same URL being handed out repeatedly. The old scan silently failed once an earlier regular re-add had replaced the registered request object with a differently-valued one, leaving a stale entry behind and appending a second one for the same unique key. Five URLs re-added over three rounds yielded 20 fetches instead of 5 and left pending_request_count at -15. Duplicates are now impossible by construction.
  • Keep the originally enqueued request on a re-add. The old code replaced the registered request with the incoming duplicate, which on a forefront re-add also replaced the queued one — resetting retry_count (a request could retry forever), label (dispatch to a different handler) and user_data. On a regular re-add it left the two out of sync, so get_request and fetch_next_request disagreed. This restores the documented contract ("Duplicates will be identified but not re-added to the queue") and matches the file-system, SQL and Redis clients, as well as @crawlee/memory-storage in Crawlee for JS.
  • Make get_request consistent after a reclaim: it now returns the reclaimed object, the same one fetch_next_request hands back.
  • Write the queue metadata once per batch instead of once per new request, saving N-1 datetime.now() calls on a batch add.

✍️ Drafted by Claude Code

vdusek

This comment was marked as outdated.

Copilot AI left a comment

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.

Pull request overview

This PR optimizes the in-memory RequestQueue client by eliminating the O(n) deque.remove scan when re-adding already-pending requests with forefront=True, replacing it with an appendleft + “lazy stale entry” skipping approach in fetch_next_request() / is_empty().

Changes:

  • Reworked MemoryRequestQueueClient.add_batch_of_requests() so forefront re-adds of pending requests no longer scan/remove from the pending deque.
  • Added stale-entry skipping in fetch_next_request() and front-pruning in is_empty() to support the new “tombstone” behavior.
  • Added regression tests to guard the performance fix and ordering/dedup semantics.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/crawlee/storage_clients/_memory/_request_queue_client.py Implements tombstone-based forefront repositioning and stale-entry skipping/pruning logic.
tests/unit/storage_clients/_memory/test_memory_rq_client.py Adds regression tests validating no deque.remove usage and preserves order/dedup semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Comment thread tests/unit/storage_clients/_memory/test_memory_rq_client.py
Comment thread tests/unit/storage_clients/_memory/test_memory_rq_client.py Outdated
@anxkhn
anxkhn force-pushed the perf/memory-rq-forefront-reposition branch from d2ca2a0 to 02429c4 Compare July 16, 2026 05:56
@vdusek
vdusek requested review from Copilot and vdusek July 17, 2026 14:18

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread tests/unit/storage_clients/_memory/test_memory_rq_client.py Outdated
Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
anxkhn added 3 commits July 27, 2026 18:33
`MemoryRequestQueueClient.add_batch_of_requests` repositioned an
already-pending request to the forefront with `deque.remove`, a linear
scan of the whole pending deque. Re-adding a batch of K already-pending
requests to a deque of N was O(K*N). Forefront re-adds are a real path:
tiered-proxy retries set `request.forefront = True` and re-enqueue while
the request is still pending.

The forefront reposition now just `appendleft`s the request and leaves
the old entry in place. The new object is registered in
`_requests_by_unique_key`, superseding the old one, and
`fetch_next_request` and `is_empty` skip entries whose object is no
longer the one registered for their unique key. Because a forefront
reposition always enqueues the live copy ahead of the entry it
supersedes, `is_empty` can safely prune stale entries from the front.

Only forefront re-adds create such tombstones. A regular re-add of an
already-pending request leaves both the deque entry and its registered
object untouched, so the identity check never misfires and no request is
dropped. Forefront-LIFO / regular-FIFO order and deduplication are
unchanged.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Replace lazy deque tombstones with an ordered mapping so forefront repositioning remains constant-time without stale-entry growth. Keep request updates on regular re-adds, update reclaimed request instances in the index, and leave is_empty side-effect free.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn
anxkhn force-pushed the perf/memory-rq-forefront-reposition branch from 02429c4 to 009c2dd Compare July 27, 2026 13:04
@anxkhn

anxkhn commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

pushed a revision addressing the latest review and ci feedback.

@vdusek

vdusek commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

@anxkhn Thank you. For future reviews, I'd really appreciate it if you could address all the feedback in a single follow-up commit, rather than splitting the changes across multiple commits, merging them into earlier commits, and force-pushing it. Your current approach makes the changes really hard to review.

@anxkhn

anxkhn commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

understood, i’ll keep future review fixes in a single follow-up commit without rewriting earlier commits.

@vdusek vdusek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks. It's much cleaner after the rewrite. Two things left.

Comment thread tests/unit/storage_clients/_memory/test_memory_rq_client.py Outdated
Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn

anxkhn commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@vdusek addressed both new threads, rebased, and re-requesting review. thank you.

@vdusek

This comment was marked as outdated.

@vdusek
vdusek requested a review from Mantisus July 28, 2026 09:32
@vdusek

vdusek commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@Mantisus could you please check this as well?

@vdusek vdusek changed the title perf(memory-storage): avoid O(n) deque scan on forefront re-add perf(memory-storage): make forefront request re-add O(1) Jul 28, 2026
@vdusek vdusek changed the title perf(memory-storage): make forefront request re-add O(1) fix(memory-storage): stop re-adds from duplicating and resetting pending requests Jul 28, 2026

@vdusek vdusek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@Mantisus Mantisus left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's a great PR, thanks for that. A couple of points from me.

Comment thread src/crawlee/storage_clients/_memory/_request_queue_client.py Outdated
Comment thread tests/unit/storage_clients/_memory/test_memory_rq_client.py Outdated
@vdusek
vdusek merged commit fae204b into apify:master Jul 29, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants