Enforce robots.txt Crawl-delay in URLFrontier queues#2000
Draft
dpol1 wants to merge 8 commits into
Draft
Conversation
When a robots.txt Crawl-delay exceeds fetcher.max.crawl.delay and the force flag makes the fetcher keep going anyway, FetcherBolt now writes the requested delay in seconds to the new robots.crawl.delay metadata key. A frontier-side consumer can then enforce the pace at the source instead of losing it, which is the gap #1979 describes for #867. The unforced path is untouched and keeps emitting Status.ERROR with error.cause=crawl_delay, pinned by a regression test.
Same signal as in FetcherBolt: when maxCrawlDelayForce caps a long Crawl-delay, the original robots value lands in robots.crawl.delay so the frontier side of #1979 sees both fetchers alike. The shared test harness moves up to AbstractFetcherBoltTest rather than being copied.
Package-private decision logic for the queue-stream consumer, same split as HostBackoff: reads robots.crawl.delay from the tuple metadata, caps it with urlfrontier.backoff.max.secs since sites do request absurd values (#1979), and deduplicates per host. An unchanged value is re-sent at most once per urlfrontier.backoff.decay.secs window, which also lets a lost fire-and-forget RPC converge. The shared _DEFAULT_ queue is skipped, as one host must not slow down the catch-all bucket.
QueueRegulatorBolt now lives up to its name: besides blocking queues on rate limits, it forwards the robots.crawl.delay carried by the queue stream to URLFrontier through setDelay, closing the loop of #1979. The call is fire-and-forget with the same short deadline as the blocks, and setDelay(key, 0) is deliberately never sent because it would erase a server-side default for the queue: a site that drops its Crawl-delay keeps the old pace, which costs throughput on that host but never politeness. The startup metadata.persist probe now checks robots.crawl.delay too, since the signal dies silently if the filter strips it.
Testcontainers regression against a real frontier. The delay does not shrink a batch, it gates the queue between hand-outs (lastProduced + delay, first hand-out always served): so the test takes one URL out, then asserts a later getURLs comes back empty although a second URL is neither served nor in-flight. The pause between the two calls also pins the unit of setDelayRequestable as seconds, a millisecond window would have expired. Seeding is generalised to seedUrls(paths...) with the old seedOneUrl() delegating, existing tests untouched.
Round fractional delays up so frontier pacing never becomes more aggressive than robots.txt. Rebuild the control signal from the locally parsed rules after protocol metadata is merged, preventing stale metadata or a colliding response header from changing it.
Keep frontier pacing opt-in and fail fast unless queues are partitioned by host, hand out one URL at a time, and carry robots.crawl.delay as persist-only metadata. Serialize setDelay calls per queue so asynchronous completions cannot violate the last-value-wins contract.
Describe the opt-in wiring and the host partitioning, single-URL hand-out, persist-only metadata, and delay-cap requirements for #1979.
Member
Author
|
While reviewing the distributed path, I found that URLFrontier 2.5 does not appear to propagate I am moving this PR back to draft while I:
The existing single-node implementation and tests remain valid. Suggestions are welcome here ;-) |
dpol1
marked this pull request as draft
July 16, 2026 06:31
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.
Closes #1979. Completes Stage 3: the queue-side politeness work discussed in #867, after explicit
Retry-Afterhandling in #1973 and adaptive back-off for headerless rate limits in #1984.When
fetcher.max.crawl.delay.forceis enabled, the fetcher caps an excessive robots.txt Crawl-delay locally and continues fetching. Until now, that meant losing the original interval and potentially crawling the host faster than requested. Both fetcher bolts now preserve the requested delay inrobots.crawl.delay, andQueueRegulatorBoltforwards it to URLFrontier throughsetDelay.Design
urlfrontier.robots.crawl.delay.enabled.CrawlDelayPolicycaps values withurlfrontier.backoff.max.secs, keeps one RPC in flight per host, and coalesces concurrent updates so the latest value eventually wins.urlfrontier.backoff.decay.secs. A failed RPC retries only a value received while the previous call was in flight; otherwise a later queue signal reasserts it.setDelay(key, 0)is deliberately never sent. If a site removes its Crawl-delay, its queue may remain slower than necessary, but the crawler never becomes less polite or erases a frontier-side default.Configuration contract
The feature fails fast unless the queue and metadata configuration can enforce a per-host delay safely:
robots.crawl.delaymust not appear inmetadata.transfer, including through wildcards such asrobots.*: an outlink must not inherit the delay of its parent host. Custommetadata.transfer.classimplementations remain responsible for preserving the same contract.URLFrontier applies the delay between queue hand-outs; it does not shrink an existing batch or recall URLs already emitted. Requiring one URL per bucket bounds each new hand-out, while already prefetched or concurrent requests may still complete during the transition.
Tests
Coverage includes both fetcher implementations, fractional delay rounding, stale and colliding metadata, opt-in and startup validation, asynchronous last-value-wins serialization, failed and stale callbacks, deduplication, caps, and a Testcontainers test against a real URLFrontier proving that the second URL remains unavailable during the configured delay.
The core and URLFrontier module suites pass with 334 and 55 tests respectively. Configuration, module README, defaults and extension documentation have been updated.
#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? (core and URLFrontier module suites pass; full reactor pending)dependencies)