Implement a Finite Concurrent Web Crawler
MongoDBSoftware Engineer · OnsiteLast updated
Implement an orchestrator for a finite concurrent web crawler featuring transactional URL state tracking, per-host politeness rate-limiting, classified error handling, and robust termination detection.
Problem Statement
Design and implement an in-process concurrent web crawler engine, ConcurrentCrawler, capable of crawling a finite set of web pages starting from a list of seed URLs. The crawler must coordinate multiple concurrent workers, ensure per-host politeness, manage transactional URL states, handle network errors with classified retries, and reliably detect when the crawl has finished.
Requirements
- URL State Machine & Deduplication: Each URL must transition through strictly tracked states:
QUEUED,FETCHING,COMPLETED, orFAILED. A URL must never be fetched more than once successfully. If the crawler is initialized with existing storage state, any URL left inFETCHINGfrom an interrupted crawl must be reset toQUEUED. - Politeness: Consecutive HTTP requests to the same host domain (e.g.,
example.com) must be spaced by at leastpoliteness_delay_msmilliseconds. Requests to different hosts may proceed concurrently. - Lossless Discovery: Links extracted from completed pages must be enqueued if they have not been seen or completed previously.
- Classified Retries: Transient fetch failures (HTTP 429, 503, network timeouts) can be retried up to 3 times before transitioning to
FAILED. Non-transient errors (HTTP 404, invalid protocol) transition directly toFAILEDwithout retries. - Termination Detection: The crawl terminates when either
max_pageshave transitioned toCOMPLETED, or when the frontier is empty and zero workers are actively fetching.
Class Interface
from typing import List, Dict, Set
from dataclasses import dataclass
@dataclass
class FetchResult:
status_code: int
outlinks: List[str]
is_transient_error: bool
class Fetcher:
def fetch(self, url: str) -> FetchResult:
"""Simulates an HTTP fetch for a URL."""
pass
class ConcurrentCrawler:
def __init__(
self,
fetcher: Fetcher,
num_workers: int,
politeness_delay_ms: int,
max_pages: int
):
pass
def crawl(self, seed_urls: List[str]) -> Dict[str, str]:
"""Runs the crawl to completion and returns a mapping of URL -> final state."""
pass
Constraints
1 <= num_workers <= 320 <= politeness_delay_ms <= 10001 <= max_pages <= 10000- URLs are valid ASCII strings containing a resolvable host.
Example
# Seeds: ["https://a.com/1", "https://b.com/1"]
# "https://a.com/1" links to ["https://a.com/2", "https://b.com/1"]
# Max pages: 5, politeness_delay_ms: 100
crawler = ConcurrentCrawler(fetcher, num_workers=2, politeness_delay_ms=100, max_pages=5)
results = crawler.crawl(["https://a.com/1", "https://b.com/1"])
# Output:
# {
# "https://a.com/1": "COMPLETED",
# "https://b.com/1": "COMPLETED",
# "https://a.com/2": "COMPLETED"
# }
# Note: "https://b.com/1" was not fetched twice.
Hints
Hint 1
Use a per-domain priority queue or last-access timestamp map guarded by a lock to enforce host-level politeness delays.
Hint 2
Termination requires tracking both the count of enqueued URLs and the count of active workers currently processing a fetch.
Hint 3
Separate the state registry (URL -> State) from the dispatch queue to guarantee atomic transitions and prevent duplicate enqueueing.
Follow-up questions
- How would you persist the frontier and URL state to MongoDB so that the crawler can resume seamlessly after a sudden process crash?
- If a single host has millions of pages while other hosts have few, how do you prevent frontier starvation without violating politeness?
- How would you adapt your termination condition to work in a distributed environment across multiple crawler nodes without a central coordinator?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.