Repository navigation
Conversation
The crawl processed pages in fixed batches of maxConcurrency and waited for the whole batch before starting the next one. One slow item therefore idled every other slot. On brendangregg.com, Guess/guess.py, guess.pl and guess.pl.txt always answer HTTP 500 (Apache runs them as CGI), and each is retried with 1+2+4 s of backoff, so the crawl stood still for about 7 s at a time. The Jobs page shows the last finished URL, which is why the reporter saw it "stop" at guess.qbas or guess.ps depending on how the batch lined up. The loop now runs a sliding window: a slot is refilled as soon as its item finishes. Discovered links are still admitted in dequeue order, not in completion order, so the queue stays breadth-first, every URL is still first offered at its shortest depth, and the crawl order does not depend on timing. Running items count against the remaining maxPages budget, so the limit is never overshot. processBatch is split into processQueueItem (one item to its outcome) and admitToQueue (deduplication and queue totals). Most of the diff is the re-indentation of that body. Fixes #490
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The crawl can resolve before all in-flight progress callbacks and storage operations finish.
Review effort: Balanced
Findings: 1
What changed in this PR
Replaces fixed-batch scraping with a sliding concurrency window while preserving crawl order and page limits.
Changes:
- Splits item processing from ordered queue admission.
- Adds concurrency, breadth-first ordering, and limit tests.
- Updates direct strategy tests for the new method.
| File | Description |
|---|---|
BaseScraperStrategy.ts |
Implements sliding-window crawling. |
BaseScraperStrategy.test.ts |
Adds concurrency regression coverage. |
WebScraperStrategy.test.ts |
Updates internal method calls. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| let nextSeq = 0; | ||
| let nextToAdmit = 0; | ||
|
|
||
| while (this.pagesIndexed < maxPages) { |
An item counts its page before its progress callback stores it. When the last pages that fit under maxPages ran concurrently, one could reach the limit while another was still storing, and the loop exited on the count alone, so scrape() resolved with a write still pending. Fixed batches never had this problem because Promise.all waited for every member. The loop now runs until nothing is left running. Reaching maxPages only stops new items from starting.
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 #490, which was reopened after 3.2.0.
What the reporter sees
After 3.2.0 the crawl of
https://www.brendangregg.comstill seems to stop, atGuess/guess.qbaswhen**/blog/**is excluded and atGuess/guess.pswhen it is not.I reproduced this on 3.2.1 with both the CLI and the web UI (with the UI's default exclude patterns). The crawl never actually hangs. It always finishes (621/621 processed, 440 indexed), but it freezes for about 7 seconds right after
guess.qbas.Why it freezes
Three files in that directory always answer HTTP 500, because Apache tries to run them as CGI scripts:
guess.py,guess.plandguess.pl.txt. The fetcher treats 500 as retryable, so each one takes 4 attempts with 1 + 2 + 4 seconds of backoff in between.The crawl loop processed pages in fixed batches of
maxConcurrency(3) and waited for the whole batch before starting the next one. So while one URL was waiting to retry, the other two slots did nothing and the whole crawl stood still.The Jobs page shows the last finished URL as "Processing …". In the directory listing,
guess.pysits betweenguess.psandguess.qbas, so the page that looks stuck depends on how the batch of 3 lines up. Excluding the blog or not changes that alignment, which matches the report exactly. The same batch barrier was also behind part of the original "freezes for a long time" report, when a large image download held up its batch.The change
The loop in
BaseScraperStrategy.scrape()now runs a sliding window. When an item finishes, the next queued item starts right away. A URL waiting to retry now only holds up its own slot.Two things stay the same on purpose:
maxPages.maxPageslimit. Items that are still running count against the remaining budget, so at mostmaxPages - pagesIndexeditems run at once and the limit is never overshot. This is the same guarantee the batch size gave before.processBatchis split intoprocessQueueItem(one item to its outcome) andadmitToQueue(deduplication and the queue totals). Most of the diff inBaseScraperStrategy.tsis the re-indentation of that body, so reviewing with whitespace hidden is easier. The twoWebScraperStrategytests that calledprocessBatchdirectly now callprocessQueueItem.Tests
Four new tests in
BaseScraperStrategy.test.ts:main.mainalready passes this. It guards the admission order: I checked that it fails if links are added in finish order, which the existing breadth-first test does not catch.maxPages.maxPagesis still being stored. This was found in review: the first version of the loop stopped as soon as the count reached the limit, soscrape()could resolve with a write still pending. The loop now runs until nothing is left running, and reachingmaxPagesonly stops new items from starting.Lint and a cold-cache typecheck are clean, and all 2329 tests pass locally.
Live check
I ran the same web UI crawl of brendangregg.com before and after the change:
guess.qbasGuess/stores within one secondAfter the change, the longest time the Jobs page went without updating was 3.1 seconds, on a slide page that has to be rendered in a browser.
Not changed here
🤖 Generated with Claude Code