README

Workshop Exercises

Setup

Run:

cargo check --workspace

This downloads dependencies and verifies everything compiles. Then run the exercise tests:

cargo test -p workshop

You should see 17 failing tests. Your goal during the workshop is to make them pass.

Crates

This workspace contains three crates:

  • workshop: exercises for blocks 1 and 2 (futures, executors, reactors)
  • mini-redis: a Redis server with built-in runtime metrics (block 3 debugging exercise)
  • redis-bench: a benchmarking tool for measuring mini-redis latency

Exercises 1-3 (workshop crate)

Run exercises with:

cargo run -p workshop -- 1.1    # Exercise 1.1 (YieldNow)
cargo run -p workshop -- 2.2    # Exercise 2.2 (spawn)
cargo run -p workshop -- 3      # Exercise 3 (echo server)

Run tests with:

cargo test -p workshop           # all tests
cargo test -p workshop ex1_1     # single exercise

Exercise 1: Implement Futures

Build async primitives by implementing the Future trait by hand.

LevelFileWhat you buildReal-world equivalent
1.1workshop/src/ex1_1.rsYieldNowtokio::task::yield_now
1.2workshop/src/ex1_2.rsDelaytokio::time::sleep
1.3workshop/src/ex1_3.rsOneshot channeltokio::sync::oneshot

Each level uses the provided executor in workshop/src/ex1_executor.rs.

Exercise 2: Build an Executor

Build a single-threaded async executor from scratch.

LevelFileWhat you buildReal-world equivalent
2.1workshop/src/ex2_1.rsblock_ontokio::runtime::Runtime::block_on
2.2workshop/src/ex2_2.rsspawn (multi)tokio::spawn

Exercise 3: Mini Reactor

Build a reactor using mio that maps OS readiness events to task wakers, then use it to run an async echo server.

LevelFileWhat you buildReal-world equivalent
3workshop/src/ex3.rsReactor + echo serverTokio's I/O driver
# Terminal 1
cargo run -p workshop -- 3

# Terminal 2
nc 127.0.0.1 3000

Hints and Solutions

Hints are in hints/, organized by exercise (e.g., hints/ex1_1/). Each exercise has numbered hint files that progress from gentle nudges to nearly-the-answer. Open them one at a time.

Reference solutions are in workshop/src/solutions/.

Debugging Exercise (mini-redis)

The mini-redis crate is a Redis server with a latency problem. Your goal is to find and fix it using Tokio's runtime metrics.

Running the server

cargo run --release -p mini-redis

The server listens on port 6379. Stop it with Ctrl-C. On shutdown, the server prints runtime metrics including the poll time histogram and per-task scheduling delay.

Benchmarking

Use the redis-bench tool to generate load and measure latency:

cargo run --release -p redis-bench

Default settings: 5 connections, 10,000 requests per command type, pipeline depth 50. Override with flags:

cargo run --release -p redis-bench -- -c 10 -n 50000 -P 500
FlagDescriptionDefault
-cNumber of parallel connections5
-nTotal requests per command type (SET and GET)10,000
-PPipeline depth (commands in flight per connection)50
--value-sizeValue payload size in bytes64
--portServer port6379

The bench tool uses raw TCP with the Redis RESP protocol. It runs a single-threaded mio event loop, one connection at a time in the pipeline. Latency is measured per pipeline batch (time from write start to first response), matching the behavior of redis-benchmark.

You can also use redis-benchmark directly:

redis-benchmark -p 6379 -t set,get -n 10000 -c 5 -P 50

Using mini-redis-cli

The mini-redis-cli tool sends individual Redis commands to the server:

cargo run -p mini-redis --bin mini-redis-cli -- set foo bar
cargo run -p mini-redis --bin mini-redis-cli -- get foo

This is useful for quick manual testing or verifying that the server is responding correctly.

Runtime metrics

The server prints two sets of metrics on shutdown:

Poll time histogram: shows how long each task poll takes. Polls should be microseconds. Polls in the hundreds-of-microseconds range indicate synchronous work blocking the runtime.

Connection handler task metrics: shows per-task scheduling delay (the gap between a task being woken and actually getting polled). High scheduling delay means the runtime cannot get to tasks fast enough, typically because another task is monopolizing the worker thread.

The server uses a single-threaded (current_thread) Tokio runtime, so all tasks share one thread. A task that does not yield blocks every other task.

Before the fix

Running the bench tool against the unpatched server with default settings:

$ cargo run --release -p redis-bench
Benchmarking 127.0.0.1:6379
  5 connections, 2000 requests each, pipeline depth 50
  Value size: 64 bytes

SET: 472628.88 requests per second, 0.02s total

Latency by percentile distribution:
    0.000% <= 0.086 milliseconds (cumulative count 1)
   50.000% <= 0.393 milliseconds (cumulative count 5000)
   75.000% <= 0.459 milliseconds (cumulative count 7500)
   93.750% <= 0.569 milliseconds (cumulative count 9375)
   99.219% <= 0.633 milliseconds (cumulative count 9921)
  100.000% <= 0.708 milliseconds (cumulative count 10000)

Summary:
  throughput: 472628.88 requests per second
  latency (msec): avg 0.391, min 0.086, p50 0.395, p95 0.589, p99 0.633, max 0.708

GET: 681092.62 requests per second, 0.01s total

Latency by percentile distribution:
    0.000% <= 0.109 milliseconds (cumulative count 1)
   50.000% <= 0.276 milliseconds (cumulative count 5000)
   75.000% <= 0.294 milliseconds (cumulative count 7500)
   93.750% <= 0.325 milliseconds (cumulative count 9375)
   99.219% <= 0.537 milliseconds (cumulative count 9921)
  100.000% <= 0.614 milliseconds (cumulative count 10000)

Summary:
  throughput: 681092.62 requests per second
  latency (msec): avg 0.277, min 0.109, p50 0.276, p95 0.326, p99 0.537, max 0.614

After stopping the server with Ctrl-C, the runtime metrics are printed:

=== Poll time histogram (421 total polls) ===

    256.00ns ..   320.00ns :        1 (  0.2%, cum   0.2%) |
    512.00ns ..   640.00ns :        1 (  0.2%, cum   1.7%) |
      2.56µs ..     3.07µs :        1 (  0.2%, cum   3.1%) |
     49.15µs ..    57.34µs :       14 (  3.3%, cum   8.3%) |###
     57.34µs ..    65.54µs :       63 ( 15.0%, cum  23.3%) |################
     65.54µs ..    81.92µs :      150 ( 35.6%, cum  58.9%) |########################################
     81.92µs ..    98.30µs :       53 ( 12.6%, cum  71.5%) |##############
     98.30µs ..   114.69µs :       48 ( 11.4%, cum  82.9%) |############
    114.69µs ..   131.07µs :       42 ( 10.0%, cum  92.9%) |###########
    131.07µs ..   163.84µs :       27 (  6.4%, cum  99.3%) |#######
    196.61µs ..   229.38µs :        2 (  0.5%, cum 100.0%) |

=== Connection handler task metrics ===

  Instrumented tasks:     10
  Dropped tasks:          10
  Total polls:            420
  Total poll duration:    34.79ms
  Mean poll duration:     82.83µs

  Total scheduled dur:    51.10ms
  Mean scheduled delay:   124.63µs

The poll time histogram shows 90%+ of polls take 49 to 163 microseconds. The mean scheduling delay of 124 microseconds means tasks wait longer to be scheduled than they spend executing. Increasing the pipeline depth or number of connections makes this worse.

After the fix

Running the same benchmark after applying the fix:

Benchmarking 127.0.0.1:6379
  5 connections, 2000 requests each, pipeline depth 50
  Value size: 64 bytes

SET: 371448.60 requests per second, 0.03s total

Latency by percentile distribution:
    0.000% <= 0.034 milliseconds (cumulative count 1)
   50.000% <= 0.065 milliseconds (cumulative count 5000)
   75.000% <= 0.079 milliseconds (cumulative count 7500)
   87.500% <= 0.093 milliseconds (cumulative count 8750)
   93.750% <= 0.110 milliseconds (cumulative count 9375)
   96.875% <= 0.151 milliseconds (cumulative count 9687)
   98.438% <= 0.182 milliseconds (cumulative count 9843)
   99.219% <= 0.240 milliseconds (cumulative count 9921)
   99.609% <= 0.245 milliseconds (cumulative count 9960)
   99.805% <= 0.245 milliseconds (cumulative count 9980)
   99.902% <= 0.245 milliseconds (cumulative count 9990)
   99.951% <= 0.245 milliseconds (cumulative count 9995)
  100.000% <= 0.245 milliseconds (cumulative count 10000)

Summary:
  throughput: 371448.60 requests per second
  latency (msec): avg 0.072, min 0.034, p50 0.066, p95 0.128, p99 0.240, max 0.245

GET: 602728.87 requests per second, 0.02s total

Latency by percentile distribution:
    0.000% <= 0.015 milliseconds (cumulative count 1)
   50.000% <= 0.033 milliseconds (cumulative count 5000)
   75.000% <= 0.040 milliseconds (cumulative count 7500)
   87.500% <= 0.045 milliseconds (cumulative count 8750)
   93.750% <= 0.063 milliseconds (cumulative count 9375)
   96.875% <= 0.075 milliseconds (cumulative count 9687)
   98.438% <= 0.081 milliseconds (cumulative count 9843)
   99.219% <= 0.119 milliseconds (cumulative count 9921)
   99.609% <= 0.130 milliseconds (cumulative count 9960)
   99.805% <= 0.130 milliseconds (cumulative count 9980)
   99.902% <= 0.130 milliseconds (cumulative count 9990)
   99.951% <= 0.130 milliseconds (cumulative count 9995)
  100.000% <= 0.130 milliseconds (cumulative count 10000)

Summary:
  throughput: 602728.87 requests per second
  latency (msec): avg 0.036, min 0.015, p50 0.034, p95 0.071, p99 0.119, max 0.130

SET p50 latency drops from 0.395ms to 0.066ms (6x improvement). GET p50 drops from 0.276ms to 0.034ms (8x). Tail latencies improve by a similar factor. Throughput stays comparable because the bottleneck was latency from scheduling delay, not raw processing speed.

The key numbers to compare:

MetricBeforeAfter
SET p50 latency0.395ms0.066ms
GET p50 latency0.276ms0.034ms
Mean poll duration82.83µs~8µs
Mean scheduling delay124.63µs~16µs

Hints

Hints are in hints/mini-redis/, numbered HINT_1.md through HINT_5.md. They progress from where to look, to what the problem is, to how to fix it. Open them one at a time.