Problem 82 · Concurrency & Python
Asyncio Crawler with Bounded Concurrency
01Problem
Implement an asyncio crawler that, given an async fetch(url) -> list[str], visits all same-host pages while never running more than N fetches at once.
02Key insight
asyncio gives cheap concurrency for IO-bound work on a single thread, so 'seen' needs no lock (no await between check and add). An asyncio.Semaphore bounds in-flight requests; hold it only around the fetch, not while recursing, to avoid deadlock.
03Solution
Try it first.
TimeO(V+E)
SpaceO(V) plus pending coroutines
tests.py · 2 passing
tests.pypython14 lines
import asyncio
pages = {"http://s.io/": [f"/p{i}" for i in range(20)] + ["http://other.io/"]}
for i in range(20):
pages[f"http://s.io/p{i}"] = ["/", f"/p{(i + 1) % 20}"]
state = {"active": 0, "peak": 0}
async def fake_fetch(url):
state["active"] += 1
state["peak"] = max(state["peak"], state["active"])
await asyncio.sleep(0.001)
state["active"] -= 1
return pages.get(url, [])
res = asyncio.run(crawl_async("http://s.io/", fake_fetch, max_concurrency=3))
assert len(res) == 21 and "http://other.io/" not in res
assert state["peak"] == 304Staff-level follow-up
What happens with millions of URLs? Recursive gather holds a coroutine per pending page; switch to a bounded asyncio.Queue with N worker tasks so memory is O(N + frontier) and back-pressure is explicit.