Lesson 42 of 44
Workshop 7: Production Practice
Each project starts with a problem and requirements, then grows through stages. Every stage is a complete program that compiles and runs. The last stage is the full program, followed by a table of its parts and extension tasks.
The projects use Testing Async Code (paused time, advance, tokio_test), Error Handling (retry and backoff), Tracing and Observability, Codecs (Framed, LinesCodec), Graceful Shutdown (CancellationToken, TaskTracker). Project 26 is a test suite, so its output is the cargo test report. The Rust blocks are not machine-checked (concept mode); their outputs were derived by reading the code. Project 27 runs under the paused clock, so retry delays cost no real time.
Project 26: Deterministic test suite for retry and rate limiting
Code that retries and throttles is about time, and tests that use real time are slow and flaky. This suite tests two small components by controlling the clock.
Requirements:
retryruns an operation up tomax_attemptstimes and waitsbase * 2^(attempt - 1)between attempts.RateLimiterallows one call pergap. Idle time is not saved up.- Tests assert exact elapsed times, not ranges.
- Tests check the state between two moments with
advance. - Tests check the waker contract with
tokio_test::task::spawn.
Stage 1: Retry with exact elapsed time
Under start_paused = true, a sleep completes as soon as the runtime has nothing else to do, and the clock jumps to the deadline. Instant::now() differences are therefore exact.
use std::future::Future;
use std::time::Duration;
use tokio::time::sleep;
pub async fn retry<T, E, F, Fut>(max_attempts: u32, base: Duration, mut op: F) -> Result<T, E>
where
F: FnMut(u32) -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let mut attempt = 1;
loop {
match op(attempt).await {
Ok(value) => return Ok(value),
Err(error) if attempt >= max_attempts => return Err(error),
Err(_) => {
sleep(base * 2u32.pow(attempt - 1)).await;
attempt += 1;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::Instant;
const BASE: Duration = Duration::from_millis(100);
#[tokio::test(start_paused = true)]
async fn first_success_waits_for_nothing() {
let start = Instant::now();
let result: Result<u32, u32> = retry(3, BASE, |n| async move { Ok(n) }).await;
assert_eq!(result, Ok(1));
assert_eq!(start.elapsed(), Duration::ZERO);
}
#[tokio::test(start_paused = true)]
async fn two_failures_wait_100_then_200() {
let start = Instant::now();
let result = retry(3, BASE, |n| async move { if n < 3 { Err(n) } else { Ok(n) } }).await;
assert_eq!(result, Ok(3));
assert_eq!(start.elapsed(), Duration::from_millis(300));
}
#[tokio::test(start_paused = true)]
async fn exhausted_retries_return_the_last_error() {
let start = Instant::now();
let result: Result<(), u32> = retry(3, BASE, |n| async move { Err(n) }).await;
assert_eq!(result, Err(3));
// Two waits: after attempt 1 and after attempt 2. None after the last.
assert_eq!(start.elapsed(), Duration::from_millis(300));
}
}running 3 tests
test tests::exhausted_retries_return_the_last_error ... ok
test tests::first_success_waits_for_nothing ... ok
test tests::two_failures_wait_100_then_200 ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sThree attempts mean two waits. The final failure returns at once, so a test that expected a third wait of 400 ms would fail.
Stage 2: A rate limiter and the waker contract
RateLimiter hands out time slots. Each call takes the later of "now" and the next free slot, moves the next slot forward by gap, and sleeps until its own slot. tokio_test::task::spawn wraps a future in a mock task that the test polls by hand. is_woken shows whether the timer woke the task. The tests poll acquire and retry directly and use advance to step the clock. The block is the full file.
use std::future::Future;
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::{sleep, sleep_until, Instant};
pub async fn retry<T, E, F, Fut>(max_attempts: u32, base: Duration, mut op: F) -> Result<T, E>
where
F: FnMut(u32) -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let mut attempt = 1;
loop {
match op(attempt).await {
Ok(value) => return Ok(value),
Err(error) if attempt >= max_attempts => return Err(error),
Err(_) => {
sleep(base * 2u32.pow(attempt - 1)).await;
attempt += 1;
}
}
}
}
pub struct RateLimiter {
gap: Duration,
next: Mutex<Instant>,
}
impl RateLimiter {
pub fn new(gap: Duration) -> Self {
RateLimiter { gap, next: Mutex::new(Instant::now()) }
}
pub async fn acquire(&self) {
let slot = {
let mut next = self.next.lock().unwrap();
let slot = (*next).max(Instant::now());
*next = slot + self.gap;
slot
};
sleep_until(slot).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::advance;
const GAP: Duration = Duration::from_millis(100);
#[tokio::test(start_paused = true)]
async fn sequential_calls_are_spaced() {
let limiter = RateLimiter::new(GAP);
let start = Instant::now();
let mut times = Vec::new();
for _ in 0..3 {
limiter.acquire().await;
times.push(start.elapsed().as_millis());
}
assert_eq!(times, [0, 100, 200]);
}
#[tokio::test(start_paused = true)]
async fn acquire_wakes_its_task_at_the_slot() {
use tokio_test::{assert_pending, assert_ready, task};
let limiter = RateLimiter::new(GAP);
assert_ready!(task::spawn(limiter.acquire()).poll()); // first call: no wait
let mut second = task::spawn(limiter.acquire());
assert_pending!(second.poll());
advance(Duration::from_millis(99)).await;
assert!(!second.is_woken());
advance(Duration::from_millis(1)).await;
assert!(second.is_woken());
assert_ready!(second.poll());
}
#[tokio::test(start_paused = true)]
async fn retry_sleeps_between_attempts() {
use tokio_test::{assert_pending, assert_ready_eq, task};
let mut run = task::spawn(retry(3, Duration::from_millis(100), |n| async move {
if n < 3 { Err(n) } else { Ok(n) }
}));
assert_pending!(run.poll()); // attempt 1 failed, sleeping 100 ms
advance(Duration::from_millis(100)).await;
assert!(run.is_woken());
assert_pending!(run.poll()); // attempt 2 failed, sleeping 200 ms
advance(Duration::from_millis(199)).await;
assert!(!run.is_woken());
advance(Duration::from_millis(1)).await;
assert_ready_eq!(run.poll(), Ok(3));
}
}running 3 tests
test tests::acquire_wakes_its_task_at_the_slot ... ok
test tests::retry_sleeps_between_attempts ... ok
test tests::sequential_calls_are_spaced ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sThe final poll of retry returns Ok(3) only after the test moved the clock by exactly 100 ms and then 200 ms.
Parts of the suite:
| Part | Role |
|---|---|
start_paused = true |
the clock moves only when the runtime is idle, so elapsed times are exact |
Instant::elapsed |
measures virtual time in assertions |
advance |
steps the clock to check state at a boundary |
task::spawn, is_woken, assert_pending! |
poll by hand and check the waker contract |
Extension tasks:
- Add a
max_delaycap toretryand test that the waits are 100, 200, 400, 400. - Make the limiter allow bursts of
ncalls and test the firstncomplete at time 0. - Add a failing test with
test_failfor a limiter that saves idle time, to show which assertion catches it.
Project 27: Capstone job-queue service
A service accepts jobs over TCP, runs them on a worker, retries failed jobs with backoff, moves jobs that keep failing to a dead-letter queue (a list of jobs given up on), keeps counters, logs with tracing, and shuts down without losing accepted jobs.
Requirements:
- The protocol is one text line per message. Commands:
SUBMIT <name> <fails>,STATS,DEAD. A job'sfailsis how many leading attempts fail (a simulation of a flaky task). - A codec turns lines into typed commands and typed replies.
- A job gets at most 3 attempts, with waits of 100 ms and 200 ms between them. A third failure sends the job to the dead-letter queue.
- Counters: submitted, done, retried, dead, pending.
- Log events go to a buffer, and the program prints them at the end, so the order is fixed.
- Shutdown stops accepting, ends connections, and lets the worker finish every queued job before the program ends.
- An in-process client on port 0 drives the run.
Stage 1: Codec, retries, backoff, and the dead-letter queue
The codec wraps LinesCodec and adds the typed layer. A parse failure becomes a Command::Invalid value, so a bad line gets an error reply and the connection stays open. The worker runs each job through up to MAX_ATTEMPTS attempts. An attempt n fails when n <= job.fails. After a failed attempt that is not the last, the worker waits 100 ms * 2^(n - 1). A job that fails every attempt goes to the dead list. STATS reports submitted, done, retried, dead, and pending counts. DEAD lists the names. The client waits until pending=0 by polling STATS.
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use futures::{SinkExt, StreamExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
use tokio::time::sleep;
use tokio_util::bytes::BytesMut;
use tokio_util::codec::{Decoder, Encoder, Framed, LinesCodec, LinesCodecError};
const MAX_ATTEMPTS: u32 = 3;
const BASE_DELAY: Duration = Duration::from_millis(100);
enum Command {
Submit { name: String, fails: u32 },
Stats,
Dead,
Invalid(String),
}
enum Reply {
Accepted(u64),
Text(String),
Error(String),
}
struct QueueCodec(LinesCodec);
impl Decoder for QueueCodec {
type Item = Command;
type Error = LinesCodecError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Command>, LinesCodecError> {
let Some(line) = self.0.decode(src)? else { return Ok(None) };
let parts: Vec<&str> = line.split_whitespace().collect();
let command = match parts.as_slice() {
["SUBMIT", name, fails] => match fails.parse() {
Ok(fails) => Command::Submit { name: name.to_string(), fails },
Err(_) => Command::Invalid(format!("bad fail count {fails}")),
},
["STATS"] => Command::Stats,
["DEAD"] => Command::Dead,
_ => Command::Invalid(format!("unknown command {line}")),
};
Ok(Some(command))
}
}
impl Encoder<Reply> for QueueCodec {
type Error = LinesCodecError;
fn encode(&mut self, reply: Reply, dst: &mut BytesMut) -> Result<(), LinesCodecError> {
let line = match reply {
Reply::Accepted(id) => format!("OK {id}"),
Reply::Text(text) => text,
Reply::Error(message) => format!("ERR {message}"),
};
self.0.encode(line, dst)
}
}
struct Job {
name: String,
fails: u32,
}
#[derive(Default)]
struct Shared {
submitted: AtomicU64,
done: AtomicU64,
retried: AtomicU64,
dead: Mutex<Vec<String>>,
}
impl Shared {
fn stats(&self) -> String {
let submitted = self.submitted.load(Ordering::SeqCst);
let done = self.done.load(Ordering::SeqCst);
let dead = self.dead.lock().unwrap().len() as u64;
format!(
"STATS submitted={submitted} done={done} retried={} dead={dead} pending={}",
self.retried.load(Ordering::SeqCst),
submitted - done - dead,
)
}
}
async fn run_job(job: &Job, shared: &Shared) {
for attempt in 1..=MAX_ATTEMPTS {
if attempt > job.fails {
shared.done.fetch_add(1, Ordering::SeqCst);
return;
}
if attempt < MAX_ATTEMPTS {
shared.retried.fetch_add(1, Ordering::SeqCst);
sleep(BASE_DELAY * 2u32.pow(attempt - 1)).await;
}
}
shared.dead.lock().unwrap().push(job.name.clone());
}
async fn worker(mut jobs: mpsc::Receiver<Job>, shared: Arc<Shared>) {
while let Some(job) = jobs.recv().await {
run_job(&job, &shared).await;
}
}
async fn handle(stream: TcpStream, jobs: mpsc::Sender<Job>, shared: Arc<Shared>) {
let mut framed = Framed::new(stream, QueueCodec(LinesCodec::new()));
while let Some(Ok(command)) = framed.next().await {
let reply = match command {
Command::Submit { name, fails } => {
let id = shared.submitted.fetch_add(1, Ordering::SeqCst) + 1;
jobs.send(Job { name, fails }).await.unwrap();
Reply::Accepted(id)
}
Command::Stats => Reply::Text(shared.stats()),
Command::Dead => Reply::Text(format!("DEAD {}", shared.dead.lock().unwrap().join(","))),
Command::Invalid(message) => Reply::Error(message),
};
if framed.send(reply).await.is_err() {
break;
}
}
}
async fn ask(client: &mut Framed<TcpStream, LinesCodec>, line: &str) -> String {
client.send(line.to_string()).await.unwrap();
client.next().await.unwrap().unwrap()
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let shared = Arc::new(Shared::default());
let (tx, rx) = mpsc::channel(16);
tokio::spawn(worker(rx, shared.clone()));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server_shared = shared.clone();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
tokio::spawn(handle(stream, tx.clone(), server_shared.clone()));
}
});
let stream = TcpStream::connect(addr).await.unwrap();
let mut client = Framed::new(stream, LinesCodec::new());
let start = tokio::time::Instant::now();
for line in ["SUBMIT resize 0", "SUBMIT upload 2", "SUBMIT export 5", "SUBMIT sync x"] {
println!("{line:<16} -> {}", ask(&mut client, line).await);
}
loop {
let stats = ask(&mut client, "STATS").await;
if stats.ends_with("pending=0") {
println!("{stats}");
break;
}
sleep(Duration::from_millis(50)).await;
}
println!("{}", ask(&mut client, "DEAD").await);
println!("virtual time: {} ms", start.elapsed().as_millis());
}SUBMIT resize 0 -> OK 1
SUBMIT upload 2 -> OK 2
SUBMIT export 5 -> OK 3
SUBMIT sync x -> ERR bad fail count x
STATS submitted=3 done=2 retried=4 dead=1 pending=0
DEAD export
virtual time: 626 msupload fails twice, so it waits 100 ms and 200 ms and succeeds on attempt 3. export fails every attempt, so it waits the same 300 ms and lands in the dead-letter queue. The virtual time is fixed because the clock is paused: it is the worker's waits plus the client's polling sleeps.
Stage 2: Tracing and graceful shutdown
Log events go into a shared buffer through a custom writer, and the program prints the buffer at the end. Each job runs inside a span with its id and name. Shutdown uses the three steps from Graceful Shutdown: cancel a CancellationToken, wait for the TaskTracker that holds the connection tasks, then wait for the worker. The worker ends when every Sender is dropped, and it drains the queue first. The client submits a last job and starts shutdown at once, to show that the accepted job still finishes.
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use futures::{SinkExt, StreamExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
use tokio::time::sleep;
use tokio_util::bytes::BytesMut;
use tokio_util::codec::{Decoder, Encoder, Framed, LinesCodec, LinesCodecError};
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use tracing::{info, instrument, warn};
const MAX_ATTEMPTS: u32 = 3;
const BASE_DELAY: Duration = Duration::from_millis(100);
enum Command {
Submit { name: String, fails: u32 },
Stats,
Dead,
Invalid(String),
}
enum Reply {
Accepted(u64),
Text(String),
Error(String),
}
struct QueueCodec(LinesCodec);
impl Decoder for QueueCodec {
type Item = Command;
type Error = LinesCodecError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Command>, LinesCodecError> {
let Some(line) = self.0.decode(src)? else { return Ok(None) };
let parts: Vec<&str> = line.split_whitespace().collect();
let command = match parts.as_slice() {
["SUBMIT", name, fails] => match fails.parse() {
Ok(fails) => Command::Submit { name: name.to_string(), fails },
Err(_) => Command::Invalid(format!("bad fail count {fails}")),
},
["STATS"] => Command::Stats,
["DEAD"] => Command::Dead,
_ => Command::Invalid(format!("unknown command {line}")),
};
Ok(Some(command))
}
}
impl Encoder<Reply> for QueueCodec {
type Error = LinesCodecError;
fn encode(&mut self, reply: Reply, dst: &mut BytesMut) -> Result<(), LinesCodecError> {
let line = match reply {
Reply::Accepted(id) => format!("OK {id}"),
Reply::Text(text) => text,
Reply::Error(message) => format!("ERR {message}"),
};
self.0.encode(line, dst)
}
}
struct Job {
id: u64,
name: String,
fails: u32,
}
#[derive(Default)]
struct Shared {
submitted: AtomicU64,
done: AtomicU64,
retried: AtomicU64,
dead: Mutex<Vec<String>>,
}
impl Shared {
fn stats(&self) -> String {
let submitted = self.submitted.load(Ordering::SeqCst);
let done = self.done.load(Ordering::SeqCst);
let dead = self.dead.lock().unwrap().len() as u64;
format!(
"STATS submitted={submitted} done={done} retried={} dead={dead} pending={}",
self.retried.load(Ordering::SeqCst),
submitted - done - dead,
)
}
}
#[instrument(skip_all, fields(id = job.id, name = %job.name))]
async fn run_job(job: &Job, shared: &Shared) {
for attempt in 1..=MAX_ATTEMPTS {
if attempt > job.fails {
shared.done.fetch_add(1, Ordering::SeqCst);
info!(attempt, "done");
return;
}
if attempt < MAX_ATTEMPTS {
shared.retried.fetch_add(1, Ordering::SeqCst);
let delay = BASE_DELAY * 2u32.pow(attempt - 1);
warn!(attempt, delay_ms = delay.as_millis() as u64, "attempt failed, retrying");
sleep(delay).await;
}
}
warn!("attempts exhausted, moved to the dead-letter queue");
shared.dead.lock().unwrap().push(job.name.clone());
}
async fn worker(mut jobs: mpsc::Receiver<Job>, shared: Arc<Shared>) {
while let Some(job) = jobs.recv().await {
run_job(&job, &shared).await;
}
info!("queue closed, worker stopped");
}
async fn handle(
stream: TcpStream,
jobs: mpsc::Sender<Job>,
shared: Arc<Shared>,
token: CancellationToken,
) {
let mut framed = Framed::new(stream, QueueCodec(LinesCodec::new()));
loop {
let command = tokio::select! {
_ = token.cancelled() => break,
frame = framed.next() => match frame {
Some(Ok(command)) => command,
_ => break,
},
};
let reply = match command {
Command::Submit { name, fails } => {
let id = shared.submitted.fetch_add(1, Ordering::SeqCst) + 1;
jobs.send(Job { id, name, fails }).await.unwrap();
Reply::Accepted(id)
}
Command::Stats => Reply::Text(shared.stats()),
Command::Dead => Reply::Text(format!("DEAD {}", shared.dead.lock().unwrap().join(","))),
Command::Invalid(message) => Reply::Error(message),
};
if framed.send(reply).await.is_err() {
break;
}
}
}
async fn serve(
listener: TcpListener,
jobs: mpsc::Sender<Job>,
shared: Arc<Shared>,
token: CancellationToken,
connections: TaskTracker,
) {
loop {
tokio::select! {
_ = token.cancelled() => break,
accepted = listener.accept() => {
let (stream, _) = accepted.unwrap();
connections.spawn(handle(stream, jobs.clone(), shared.clone(), token.clone()));
}
}
}
}
#[derive(Clone)]
struct LogBuffer(Arc<Mutex<Vec<u8>>>);
impl Write for LogBuffer {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
async fn ask(client: &mut Framed<TcpStream, LinesCodec>, line: &str) -> String {
client.send(line.to_string()).await.unwrap();
client.next().await.unwrap().unwrap()
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let log = LogBuffer(Arc::new(Mutex::new(Vec::new())));
let writer = log.clone();
tracing_subscriber::fmt()
.with_writer(move || writer.clone())
.with_ansi(false)
.without_time()
.with_target(false)
.init();
let shared = Arc::new(Shared::default());
let token = CancellationToken::new();
let connections = TaskTracker::new();
let (tx, rx) = mpsc::channel(16);
let worker_task = tokio::spawn(worker(rx, shared.clone()));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(serve(listener, tx, shared.clone(), token.clone(), connections.clone()));
let stream = TcpStream::connect(addr).await.unwrap();
let mut client = Framed::new(stream, LinesCodec::new());
for line in ["SUBMIT resize 0", "SUBMIT upload 2", "SUBMIT export 5", "SUBMIT sync x"] {
println!("{line:<16} -> {}", ask(&mut client, line).await);
}
loop {
let stats = ask(&mut client, "STATS").await;
if stats.ends_with("pending=0") {
println!("{stats}");
break;
}
sleep(Duration::from_millis(50)).await;
}
println!("{}", ask(&mut client, "DEAD").await);
println!("SUBMIT last 1 -> {}", ask(&mut client, "SUBMIT last 1").await);
println!("shutting down");
token.cancel();
server.await.unwrap();
connections.close();
connections.wait().await;
worker_task.await.unwrap();
println!("{}", shared.stats());
println!("--- log ---");
print!("{}", String::from_utf8(log.0.lock().unwrap().clone()).unwrap());
}SUBMIT resize 0 -> OK 1
SUBMIT upload 2 -> OK 2
SUBMIT export 5 -> OK 3
SUBMIT sync x -> ERR bad fail count x
STATS submitted=3 done=2 retried=4 dead=1 pending=0
DEAD export
SUBMIT last 1 -> OK 4
shutting down
STATS submitted=4 done=3 retried=5 dead=1 pending=0
--- log ---
INFO run_job{id=1 name=resize}: done attempt=1
WARN run_job{id=2 name=upload}: attempt failed, retrying attempt=1 delay_ms=100
WARN run_job{id=2 name=upload}: attempt failed, retrying attempt=2 delay_ms=200
INFO run_job{id=2 name=upload}: done attempt=3
WARN run_job{id=3 name=export}: attempt failed, retrying attempt=1 delay_ms=100
WARN run_job{id=3 name=export}: attempt failed, retrying attempt=2 delay_ms=200
WARN run_job{id=3 name=export}: attempts exhausted, moved to the dead-letter queue
WARN run_job{id=4 name=last}: attempt failed, retrying attempt=1 delay_ms=100
INFO run_job{id=4 name=last}: done attempt=2
INFO queue closed, worker stoppedlast 1 was accepted immediately before token.cancel(). The connection task ends, the server drops its Sender, and the worker still runs last (one retry, then done) before it sees the closed queue and stops. The counters after shutdown include that job.
Parts of the service:
| Part | Role |
|---|---|
QueueCodec |
Decoder and Encoder over LinesCodec; text lines to Command and Reply |
Shared |
atomic counters and the dead-letter list |
worker, run_job |
one queue consumer; attempts, backoff, dead-lettering, one span per job |
handle, serve |
per-connection loop and accept loop, both stopped by the token |
TaskTracker |
lets shutdown wait for connection tasks |
LogBuffer |
a writer that collects log lines so the program prints them in a fixed place |
Extension tasks:
- Add a
SHUTDOWNcommand that cancels the token from inside a connection. - Run two workers that share the queue behind a
Semaphore-bounded pool, and print only totals so the output stays fixed. - Add a
RETRY <name>command that moves a job from the dead-letter queue back to the main queue.
Interview points
- Under
start_paused = true, virtual time advances only when the runtime is idle, so elapsed-time assertions can be exact. advancechecks the state between two moments. Yield after it so other tasks run.tokio_test::task::spawnwithis_wokentests the waker contract without an executor thread.- A retry with
nattempts hasn - 1waits. - A time-slot rate limiter takes
max(now, next_slot)and does not save idle time. - A codec keeps parsing out of connection handlers and turns bad input into a value instead of an error that closes the stream.
- Graceful shutdown order: stop accepting, end connections, wait for the tracker, drop every
Sender, then wait for the consumer to drain. - A bounded queue gives submitters backpressure when the worker is slow.
Summary
- Deterministic tests use
start_paused,Instant::elapsed,advance, andtokio_test. - Project 26 tests
retryandRateLimiterwith exact millisecond assertions. - Project 27 combines
Framedwith a custom codec, retries with exponential backoff, a dead-letter queue, atomic counters, tracing spans, and drain-on-shutdown. - The service runs on
127.0.0.1:0with an in-process client, so its output is fixed.
