GOMS. 2026 · Term 1

Mini 1: Distributed Crawler

Some of you have written a web crawler before, maybe in Java with Jsoup and Apache Commons HTTP. In this mini, you'll write one again, this time in Rust (async, on top of tokio), with a twist: the crawl is split across several crawler processes (nodes). Each node crawls many pages in parallel, and the nodes coordinate through a shared Redis instance. You'll drive the whole thing from a small command-line tool.

The Task

Given a base path, crawl every page reachable from it and collect a few statistics about what you find. The result of a crawl is a WebStats:

pub struct WebStats {
    // the total number of (unique) files found
    pub num_files: usize,
    // the total number of (unique) file extensions (.jpg is different from .jpeg)
    pub num_exts: usize,
    // the total number of files for each extension
    pub ext_counts: HashMap<String, usize>,
    // the total number of words in all HTML files combined, excluding
    // all HTML tags, attributes, and HTML comments
    pub total_word_count: u64,
}

Files. A file is any URL under the base path that you find a link to and that actually exists: HTML pages, but also images, zips, PDFs and so on. Broken links don't count. You don't need to download a whole zip to know it's there; a HEAD request will do.

Same URL or not? Drop the fragment, so page.html#intro and page.html#usage are both just page.html. For other corner cases, do what's reasonable and mention your choices in the README.

Words and extensions. A page counts as HTML if its Content-Type says so, whatever its URL looks like. When counting words, lowercase the text first (so Ant and ant are the same word) and only count strings that begin with a-z. Beyond that, our definition of a word is pretty loose, so do what's reasonable. Extensions are lowercased too, so JPEG and jpeg are the same extension. (jpg and jpeg are still different.) A URL with no extension, such as /api/ or /docs/intro, counts as html.

The base path. As a familiar example, submitting

https://cs.muic.mahidol.ac.th/courses/ooc/api/

means the crawl starts at that page and only retrieves URLs that begin with this base path. This matters. If you follow links outside the base path, you may end up crawling a good chunk of the Internet. When you crawl a page p, parse it fully and follow every hyperlink on p that falls under the base path.

We won't publish "correct" stats for any site. Pick a few sites of your own to try, and compare notes with your classmates.

The CLI

Users interact with the cluster through a single binary (call it crawl, or something better). It should support at least the following:

A session might look something like this. The exact output format is up to you.

$ crawl submit https://cs.muic.mahidol.ac.th/courses/ooc/api/ https://example.org/docs/
job 7f3a  https://cs.muic.mahidol.ac.th/courses/ooc/api/
job 91c2  https://example.org/docs/

$ crawl status 7f3a
crawled 812   frontier 140   in flight 24   running

$ crawl status -f 7f3a
crawled 836   frontier 131   in flight 24   running
crawled 901   frontier 88    in flight 24   running
...
crawled 1540  frontier 0     in flight 0    done

$ crawl stats 7f3a
files: 1540   extensions: 6   words: 1283077
  html 1391   css 12   js 41   png 88   gif 7   zip 1

The CLI talks only to Redis, not to the nodes directly. That keeps things simple, and it means the CLI works no matter how many nodes are running.

Core Design Requirements

Concurrency. One effective way to avoid crawling pages twice is breadth-first search. Given the current frontier, you crawl all of its pages in parallel, across nodes and within each node, to build the next frontier. Another way is to drop the lock-step rounds and keep one shared work queue that nodes pull from and push to. Both work, and there are others. Choose whichever you can argue is correct.

Exclusionary

You don't need to honor robots.txt or implement politeness policies beyond the per-node cap above. You don't need to run JavaScript or handle pages that are rendered client-side, you don't need to cancel jobs, and you don't need to recover from crashed nodes.

How to hand in

Getting Started & Hand-in Instructions

This mini is individual. You'll work in your own GitHub repository.

Demo day. You'll demo your crawler live, running several nodes plus the CLI. Find another person to help you on the day. Once you've demoed, make the repository public so we can grade it.

Tips and Pointers:

Extra Challenge (for No Real Credit)

Make your crawler survive a node being killed mid-crawl. The job should still finish, with every page counted exactly once. Leases or timeouts on claimed URLs are a good place to start.