Enforce robots.txt Crawl-delay in URLFrontier queues#2000
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.
|
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 ;-) |
…nt only The delay signal is now conservative: the maximum observed value wins and a lower delay applies only after the previous maximum expires from the decay window. The cap and dedupe window move to dedicated keys (urlfrontier.robots.delay.max.secs / .decay.secs) instead of reusing the back-off ones. A failed RPC no longer self-retries without a higher pending value: its value is kept as an ambiguous floor for the decay window - the server may have applied it before the response was lost - so the next signal is not deduplicated and reasserts at least that value instead of lowering a possibly applied delay. Keyed setDelay/blockQueueUntil are not propagated across URLFrontier nodes (crawler-commons/url-frontier#146): multiple configured addresses now log a warning bolt-wide and fail fast only when robots pacing is enabled; the single host/port fallback stays valid. Javadoc, README and the asciidoc reference document the dedicated keys, the connected-node-only semantics and the non-atomic getURLs politeness gate under concurrent clients. QueueRegulatorBoltTest folds the two blind waits into one.
|
Done in e2cda7a: pacing now coalesces to the max observed delay (a failed RPC keeps its value as a floor for the decay window, so a lower signal can't undercut it), the cap and dedupe window moved to dedicated urlfrontier.robots.delay.* keys, and multiple frontier addresses fail fast only when robots pacing is enabled (just a warning otherwise, matching the #1984 behaviour). While double-checking the concurrent path I also found the getURLs politeness gate itself is racy under concurrent clients — filed as crawler-commons/url-frontier#147 and documented here rather than enforced. PR body updated, marking as ready. |
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 with the dedicatedurlfrontier.robots.delay.max.secs, keeps one RPC in flight per host, and coalesces concurrent updates conservatively: the maximum observed value wins, so a shorter delay cannot lower the pace of a shared queue. A lower observed delay can be applied only after the previously successful maximum expires from the decay window.urlfrontier.robots.delay.decay.secs. A failed RPC self-retries only when a higher value arrived while the call was in flight; otherwise its value is kept as an ambiguous floor for the decay window (the server may have applied it before the response was lost) and the next queue signal reasserts at least that value.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.
Deployment model
Keyed
setDelay/blockQueueUntilcalls are not propagated across URLFrontier nodes: in 2.5 the distributed service ignoreslocal=falsefor these RPCs (crawler-commons/url-frontier#146, confirmed upstream; fix in progress). Consequences here:urlfrontier.addressentries the bolt warns (rate-limit blocks from Adaptive back-off for rate-limited hosts via the queue stream #1984 stay best-effort on the connected node, unchanged behaviour) and fails fast only when robots pacing is enabled, since silent partial pacing would violate robots.txt. A single endpoint through theurlfrontier.host/urlfrontier.portfallback stays valid.getURLsis not atomic (getURLs: per-queue politeness gate is not atomic under concurrent requests crawler-commons/url-frontier#147): with several concurrentgetURLsclients (multiple Spout tasks or topologies) a queue can be served more than once inside the delay window. This is not limited to the transition to a new delay; it can recur on every concurrently aligned refill. Documented; a single Spout task per frontier is recommended when pacing must be strict.Tests
Coverage includes both fetcher implementations, fractional delay rounding, stale and colliding metadata, opt-in and startup validation (including the multi-address warn/fail split), asynchronous max-wins serialization, failed and stale callbacks, the ambiguous floor, deduplication and the decay window for lower values, 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 60 tests respectively. Configuration, module README, defaults and extension documentation have been updated.
For all changes
#XXXXwhereXXXXis the issue number you are trying to resolve? (title kept descriptive; the issue is referenced in the first line and in the commits)mvn git-code-format:format-code -Dgcf.globPattern="**/*" -Dskip.format.code=false?For code changes
mvn clean verify? (core and URLFrontier modules, with the Testcontainers integration tests)