perf(memory-storage): make forefront request re-add O(1) - #2074
Conversation
There was a problem hiding this comment.
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 pendingdeque. - Added stale-entry skipping in
fetch_next_request()and front-pruning inis_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.
d2ca2a0 to
02429c4
Compare
`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>
02429c4 to
009c2dd
Compare
|
pushed a revision addressing the latest review and ci feedback. |
|
@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. |
|
understood, i’ll keep future review fixes in a single follow-up commit without rewriting earlier commits. |
vdusek
left a comment
There was a problem hiding this comment.
Thanks. It's much cleaner after the rewrite. Two things left.
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
|
@vdusek addressed both new threads, rebased, and re-requesting review. thank you. |
|
Summary:
|
|
@Mantisus could you please check this as well? |
MemoryRequestQueueClient.add_batch_of_requestsrepositioned an already-pendingrequest to the forefront by calling
self._pending_requests.remove(existing_request)on the pending
deque.deque.removeis O(n): it linearly scans the deque to findthe entry. Re-adding a batch of K already-pending requests to a pending deque of N
entries is therefore O(K*N).
Forefront re-adds of still-pending requests are a normal runtime path, not an edge
case:
RequestManagerTandem.fetch_next_requestre-enqueues every request it takes fromthe read-only loader with
forefront=True(
src/crawlee/request_loaders/_request_manager_tandem.py).request.forefront = Truebefore re-adding(
src/crawlee/proxy_configuration.py).This change removes the linear scan:
appendlefts the request and leaves the oldentry in the deque. The new object is registered in
_requests_by_unique_key,superseding the old one.
fetch_next_requestandis_emptyskip any popped entry whose object is no longerthe one currently registered for its
unique_key(a stale tombstone). Because aforefront reposition always enqueues the live copy ahead of the entry it supersedes,
is_emptycan safely prune stale entries from the front until it reaches a live one.no-op: neither the deque entry nor its registered object is touched, so the identity
check never misfires and no request is dropped. This matches the sibling file-system
client, which likewise does not re-store an already-pending request on a
non-forefront re-add.
Forefront-LIFO / regular-FIFO ordering, deduplication, and the metadata counts
(
total/pending/handled) are unchanged.This is the memory-client counterpart to #2011, which fixes the same class of
quadratic reposition behavior in the file-system request queue client. The memory
client was not covered there.
Issues
Testing
Added regression tests in
tests/unit/storage_clients/_memory/test_memory_rq_client.py:test_forefront_readd_repositions_without_deque_scan- wraps the pending deque in adequesubclass that countsremovecalls; re-adding 20 still-pending requests withforefront=Truenow performs 0removecalls (was 20 before the change). Guards theperformance fix.
test_forefront_readd_preserves_order_and_dedup- forefront re-add of a subset keepsLIFO order, no duplicates linger, and the queue drains to empty/finished.
test_regular_readd_of_pending_request_is_not_dropped- a non-forefront re-add of astill-pending URL as a distinct
Requestobject stays fetchable exactly once, withconsistent counts (guards against the lazy-tombstone skip dropping a live request).
test_regular_readd_does_not_reorder_pending_queue- a non-forefront re-add leavesFIFO order untouched.
Commands run locally (offline):
Checklist