#2129 FetcherBolt: lock-free fetch queues ordered by next fetch time - #2132
Open
GGraziadei wants to merge 3 commits into
Open
#2129 FetcherBolt: lock-free fetch queues ordered by next fetch time#2132GGraziadei wants to merge 3 commits into
GGraziadei wants to merge 3 commits into
Conversation
… time FetchItemQueues used a single monitor for adding, taking and finishing items, and getFetchItem() rotated a LinkedHashMap linearly over all the queues that were not ready yet. With many hosts the fetcher threads held that lock most of the time and the executor thread calling execute() stalled on every incoming tuple. Queues are now kept in a ConcurrentHashMap and the ones that may have an item ready are referenced from a DelayQueue of tickets ordered by their next fetch time, so taking an item is O(log n) and adding never waits for the fetcher threads. Per-queue state (size bound, in-progress count, next fetch time, crawl delays) is handled with atomics and a per-queue monitor only for the add/reap race. Empty queues are removed from the map as soon as they drain. Fixes apache#2129. Behaviour preserved: politeness per queue, asap release, max threads per queue, max queue size, crawl delay overrides from metadata and robots.txt, queue modes, metrics and the debug dump. Covered by the new FetchItemQueuesTest, including a concurrent producers/consumers test. Benchmark (50 fetcher threads, 1 producer, 20 URLs per host, delay 1s): 20000 hosts getFetchItem avg 1.58 ms -> 24 us addFetchItem p99 458 ms -> 0.11 ms, max 571 ms -> 0.8 ms throughput unchanged (bounded by politeness)
rzo1
reviewed
Sep 6, 2026
rzo1
left a comment
Contributor
There was a problem hiding this comment.
Currently I am short on time, so first round AI-based review)
The design holds up: one ticket per queue guarded by the scheduled CAS, an immutable ordering key in QueueTicket, and FetchItem.create (which does DNS in byIP mode) moved out of the critical section.
One lost-wakeup window, inline on line 538.
Other comments:
- Fairness changes from
LinkedHashMapround-robin to strictnextFetchTimeorder, with arbitrary tie-breaking among queues sharing a millisecond. This is a component operators tune by feel, so please put the change in the description. offerincrementssizebefore the bound check and decrements on overflow, sogetQueueSize()can transiently over-report. Harmless, but worth a comment.logQueuesContentis no longer synchronized. Safe with weakly consistent iterators, but the dump is now a smear rather than a snapshot. Say so in its javadoc.- This rewrites the hottest path in the crawler. Please include before/after numbers for the case it targets, and confirm the existing
FetcherBoltintegration tests pass.
…shes during the free-slot check In getFetchItem(), a fetch on the same queue could finish between hasFreeSlot() returning false and the clearing of the scheduled flag. finishFetchItem() then found the flag still set and did not issue a ticket, after which the flag was cleared: the queue was left with items and a free slot but no ticket, and stalled until an unrelated URL for the same host arrived. Clear the flag first and re-check, as the other branches already do. Covered by a deterministic test that pauses the poller inside the free-slot check while the in-progress fetch finishes.
…fer() and the smeared debug dump offer() increments the size before checking the bound and decrements on overflow, so getQueueSize() can transiently over-report by one while an offer is rejected; the value only feeds metrics and the debug dump. logQueuesContent() is no longer synchronized: it iterates the queues and their items with weakly consistent iterators while the fetcher threads keep working, so the dump is a smear rather than a point-in-time snapshot. Both are now stated in the javadoc.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2129.
FetchItemQueuesused a single monitor for adding, taking and finishing items, andgetFetchItem()rotated aLinkedHashMaplinearly over every queue whose crawl delay had not elapsed. With many hosts the fetcher threads held that lock most of the time and the executor thread callingexecute()stalled on every incoming tuple.Change
ConcurrentHashMap; the ones that may have an item ready are referenced from aDelayQueueof tickets ordered by their next fetch time. Taking an item is O(log n) and adding never waits for the fetcher threads.FetchItemis built before any lock is taken, so a DNS lookup inbyIPmode no longer blocks the fetcher threads.Behaviour preserved
Politeness per queue,
asaprelease,fetcher.threads.per.queueandfetcher.maxThreads.*,fetcher.max.queue.size, crawl delay overrides from metadata and robots.txt, queue modes, metrics and the debug dump.Behaviour change: order in which hosts are served
The old implementation served hosts round-robin in
LinkedHashMapinsertion order, rotating past the ones whose crawl delay had not elapsed. The new one serves them in strictnextFetchTimeorder: the host that became ready the longest time ago goes first, and hosts whose next fetch time falls in the same millisecond are ordered arbitrarily (DelayQueuedoes not guarantee FIFO among equal delays).In practice:
Two smaller observable details, both stated in the javadoc:
getQueueSize()can transiently over-report by one while an offer is being rejected, and the debug dump produced by the trigger file is no longer a point-in-time snapshot but a smear over the time it takes to iterate.Tests
New
FetchItemQueuesTest: politeness,asap, several hosts back to back, queue size bound, multiple threads per queue, crawl delay from metadata, reaping of empty queues, and a concurrent producers/consumers test checking that nothing is lost or duplicated. ExistingFetcherBoltTestandSimpleFetcherBoltTestpass unchanged.Benchmark: the case this targets
The cost the change removes is the linear rotation over queues that are not ready. It shows when a fetcher instance holds many hosts and most of them are waiting on their crawl delay, which is the steady state of a broad crawl: every
getFetchItem()walked all of them under the global monitor, and the executor thread callingexecute()queued behind the fetcher threads for that monitor.Micro-benchmark on
FetchItemQueuesalone (mainvs this branch): 50 fetcher threads, 1 producer thread emulating the executor, 20 URLs per host,fetcher.server.delay1s so that almost every queue is waiting, instant fetch, 8s per run, p99 from sampled calls.getFetchItemavggetFetchItemp99addFetchItemavgaddFetchItemp99addFetchItemp99Throughput is unchanged, as expected: it is bounded by politeness. What goes away is the executor stall and the CPU burnt scanning queues that are not ready. With
fetcher.server.delay0 (every queue always ready, no rotation) both versions do ~53k fetch/s, i.e. the new structure is not slower on the path where the old one was cheap.With a few hundred hosts per fetcher instance the difference is not measurable; with tens of thousands it is the numbers above.
Verification
FetcherBoltTest(8) andSimpleFetcherBoltTest(8) pass unchanged;FetchItemQueuesTestadds 9 tests including a concurrent producers/consumers run and a deterministic reproduction of the lost-wakeup race fixed in the second commit.mvn clean verifyon the whole project: 675 tests, 0 failures, 0 errors (Docker-backed modules included).For all changes
#XXXXwhereXXXXis the issue number you are trying to resolve?mvn git-code-format:format-code -Dgcf.globPattern="**/*" -Dskip.format.code=false?For code changes
mvn clean verify?