Fetch WARC records in coalesced ranged GETs - #84
Merged
Merged
Conversation
One record per HTTP request is fine for a few thousand locations and hopeless for a few million. --batch sorts the locations by file and offset, coalesces the ones that sit within --gap of each other, and reads each run of them in a single ranged GET. Each record is sliced back out of the coalesced span by its own offset and length and parsed on its own, so the batch path is byte identical to the one at a time path by construction rather than by resemblance. Verified on 486 real records from Common Crawl: same 486 files, no diff. --gap is the price you pay in wasted bytes for one saved request, and --dry-run reports both halves of that trade without fetching anything, since the grouping is pure arithmetic on the locations. On a million real robots.txt locations the default gap gives 9.9x fewer requests for 71x the bytes, and that is still the right call: on 486 records packed into 20 files it ran in 6.6s against 97.4s for the one at a time path. --ledger makes a run resumable. It holds one filename@offset per line, flushed after every group, so a kill costs at most the groups in flight. --order input puts back the ordering the grouping destroyed, at the cost of holding finished records in memory until their turn comes, which the docs say plainly rather than leaving to be discovered. FileURL now passes an absolute URL through untouched, the same way resolvePartURL already treated a manifest entry, which is what lets the tests point the fetch path at a local server. Refs #48
7 tasks
This was referenced Aug 10, 2026
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 #48.
One record per HTTP request is fine for a few thousand locations and hopeless for a few million.
--batchsorts the locations by file and offset, coalesces the ones that sit within--gapbytes of each other, and reads each run of them in a single ranged GET.Each record is sliced back out of the coalesced span by its own offset and length and parsed on its own, so the batch path is byte identical to the one at a time path by construction rather than by resemblance.
Flags on
ccrawl fetch--batch--gap--max-span--orderinputorfile: emit in the order given or the order on disk (defaultfile)--ledger--lookahead--dry-rungroups the locations and reports what the run would ask for without asking for any of it, which is the free way to pick a gap.Done when, checked against real Common Crawl
1M locations fetched with at least 3x fewer HTTP requests. A million real robotstxt locations from CC-MAIN-2026-30, 99997 distinct WARC files, median 10 records per file:
9.9x at the default gap. The 71x byte amplification is the honest other half of the trade and it is in the docs, not hidden: a round trip to
data.commoncrawl.orgcosts far more than a megabyte of transfer does. Measured on 486 real records packed into 20 files, both paths at 8 workers:14.7x faster while reading 34x the bytes.
Record output is identical to the one at a time path. Those same 486 real records fetched both ways into two directories,
diff -rqclean.A SIGKILL mid run resumes without refetching.
kill -9after 3 seconds left 50 locations in the ledger and 50 files on disk. Rerunning the same command skipped those 50, fetched the remaining 436, and the finished directory diffs clean against the unbroken run.Notes
FileURLnow passes an absolute URL through untouched, the same wayresolvePartURLalready treated a manifest entry.DataBaseURLis a const so a test cannot override it, and this is what lets the tests point the fetch path at a local server. No real Common Crawl path starts with a scheme, so it costs nothing.--order filestreams under a hard--lookaheadcap.--order inputhas to put back an ordering the grouping destroyed, so it holds finished records until their turn comes, and one slow group early in the input holds everything behind it. The docs say so rather than leaving it to be discovered.scripts/docs-drift.shtreated a third level prose heading as a reset to the whole page scope, so### Batch modeunder## fetchreported every flag as missing. A third level heading now keeps the scope of the command above it.Tests: grouping (span cap, unsorted input, duplicate records), batch output against
FetchWARCRecordrecord for record with a request count assertion, both orderings, a resume with an exact request count, a single bad location among good ones, and the ledger itself.