fix(robots): cache misses and share in-flight robots.txt fetches - #39
Open
Mr-Neutr0n wants to merge 1 commit into
Open
fix(robots): cache misses and share in-flight robots.txt fetches#39Mr-Neutr0n wants to merge 1 commit into
Mr-Neutr0n wants to merge 1 commit into
Conversation
A domain whose robots.txt is unreachable was re-fetched on every URL check, and N concurrent checks on one domain each issued their own request because the cache lock was released across the fetch. Cache the miss under a shorter TTL and join an in-flight fetch per domain instead of starting another.
dondai44423
added a commit
to dondai44423/master-fetch
that referenced
this pull request
Jul 30, 2026
… domain A domain without robots.txt was re-fetched on every URL check, and N concurrent checks on one domain issued N fetches. Misses now cache for 5 min; concurrent checks share one in-flight task. Co-authored-by: community PR dondai1234#39
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.
Two gaps in the robots.txt cache, both of which cost real time during a crawl. I hit this with
respect_robots=trueover a domain that has norobots.txt.1. Misses are never cached
_get_robots_parseronly writes to_robots_cacheon the success path. When_fetch_robots_txtreturnsNone— domain has no robots.txt, or it is temporarily unreachable — nothing is cached, so the next URL on that domain fetches again. And again.That is not just a wasted request.
_fetch_robots_txttries theHTTPSessionpath first and then falls through to the urllib fallback, each with_FETCH_TIMEOUT = 10. On a domain that black-holes the request, every single URL in the crawl can pay up to ~20s before the fetch is given up on and the URL is allowed by default.2. Concurrent checks on one domain each fetch separately
The cache lock is released before the fetch and re-taken after it:
So every caller that arrives while the first fetch is in flight misses the cache and starts its own.
crawl.pyandbulk_fetchcheck URLs on one domain concurrently, which is exactly the shape that triggers it.Measured against
main, with_fetch_robots_txtstubbed to count calls:After this change:
The change
_robots_cachenow storesRobotFileParser | None, so a miss is a cache entry rather than the absence of one. Misses expire on a separate_ROBOTS_MISS_TTL(5 min) rather than the 1 hour used for hits — an unreachable robots.txt is often transient, and I did not want a five-second blip to suppress robots for the next hour._load_robots_parser, run as one shared task per domain and tracked in_robots_inflight. Callers that arrive mid-fetchawaitthe existing task instead of starting a second one. The task clears its own entry in afinally.asyncio.shieldon the await, so one caller being cancelled does not cancel the fetch the other callers are waiting on.Single-flight is keyed per domain, so unrelated domains still fetch in parallel — there is a test for that, since it would be an easy thing to break with a single global lock.
Behaviour is otherwise unchanged: unreachable and unparseable robots.txt still allow by default, and
clear_robots_cache()still clears everything.Tests
Four added to
TestIsAllowed, alongside the existingtest_cache_prevents_refetch:test_unreachable_robots_is_cached— 5 URLs, 1 fetchtest_concurrent_checks_share_one_fetch— 20 concurrent URLs, 1 fetchtest_concurrent_checks_on_distinct_domains_are_not_serialized— guards against fixing this with a global locktest_cached_miss_expires— miss TTL is honouredThe first two fail on
mainwithassert 5 == 1andassert 20 == 1.pytest tests/test_robots.py→ 15 passed. Full suite761 → 765passed with the same 2 pre-existingtest_browser_lifecyclefailures before and after (they need a real playwright install).I use AI assistance in my workflow; the numbers above are from runs against
86d1b13.