Learn / Rust Practical Systems
CLI, Async Persistence, and Release
Create a companion CLI, persist service state with sqlx-style repositories, and run release checks.
Course: Rust Practical Systems. Level: Intermediate. Topic: Systems reliability.
Stage: intermediate - Intermediate async tooling - CLI, async persistence, and release commands. Connect a Rust service to a companion CLI, async persistence boundary, and repeatable release checks.
Outcomes
- Parse typed command-line arguments.
- Use async repositories behind application state.
- Run fmt, clippy, tests, and documented release commands.
- Explain why async functions return futures that must be awaited or spawned
- Use Tokio tasks, async I/O, select, timeout, and cancellation-aware design
- Avoid common async pitfalls around blocking and locks
- Write unit tests, integration tests, and doc tests
- Use test organization to verify public APIs and edge cases
- Add quality gates with formatting, linting, property tests, and coverage-aware thinking
Concepts
- clap
- tokio
- sqlx pool
- cargo clippy
- Async Rust and Tokio
- Async functions are lazy futures
- Tasks, timeouts, and blocking
- Rust for Reliable Systems and Services foundations
- Guided practice
- Testing and Quality
- Built-in tests
- Beyond example tests
Concept flow
Show how cli, async persistence, and release commands moves from trigger to implementation outcome in Rust Systems.
- CLI command
- HTTP client
- Axum API
- Repository
- Database
- Release checks
Session flow
- Model clap (concept, 12 min) — Name the decisions behind clap before writing code.
- Parse typed command-line arguments.
- Explain where clap belongs in job orchestration service.
- Build the vertical slice (walkthrough, 22 min) — Implement the smallest useful slice in src/bin/jobctl.rs.
- Use async repositories behind application state.
- Connect tokio to a working example.
- Verify and harden (exercise, 14 min) — Run cargo fmt, clippy, and test before release.
- Run fmt, clippy, tests, and documented release commands.
- Record one risk or follow-up before moving on.
- Futures and the Tokio Runtime: Async functions are lazy futures (concept, 60 min) — Calling an async function creates a future. The future does not make progress until it is awaited or polled by an executor. Tokio provides an executor, timers, TCP, channels, and other async-aware utilities.
- Retained source example: Tokio entry point
#[tokio::main]
async fn main() {
let body = fetch_status().await;
println!("{body}");
}
async fn fetch_status() -> &'static str {
"ok"
}
Expected output: ok
The tokio::main macro creates a runtime and blocks the main thread on the async main future.
- Futures and the Tokio Runtime: Tasks, timeouts, and blocking (walkthrough, 60 min) — tokio::spawn schedules a future as a task. Use timeouts for bounded waits, select for racing events, and spawn_blocking for CPU-heavy or blocking work. Blocking inside async tasks can starve the runtime's worker threads.
- Retained source example: Timeout around async work
use std::time::Duration;
use tokio::time::timeout;
async fn call_service() -> Result<String, &'static str> {
let result = timeout(Duration::from_secs(2), async {
"response".to_string()
})
.await
.map_err(|_| "service timed out")?;
Ok(result)
}
timeout returns an error if the inner future does not complete before the duration.
- Futures and the Tokio Runtime: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Use async-aware synchronization such as tokio::sync when guards must interact with awaits.
- Practice: Avoid blocking calls inside async tasks; use async APIs or spawn_blocking.
- Practice: Propagate cancellation by letting futures drop cleanly and by avoiding detached tasks without supervision.
- Avoid: Holding a synchronous Mutex guard across an await point.
- Avoid: Spawning tasks and ignoring their JoinHandles when results or failures matter.
- Avoid: Assuming async makes CPU-bound code faster.
- Futures and the Tokio Runtime: references (review, 2 min) — Original references retained from the legacy library.
- Async book: https://rust-lang.github.io/async-book/
- Tokio tutorial: https://tokio.rs/tokio/tutorial
- Tokio docs: https://docs.rs/tokio/latest/tokio/
- Testing Rust Code: Built-in tests (concept, 53 min) — Rust's test runner is built into Cargo. Unit tests usually live beside the code under cfg(test), while integration tests live in the tests directory and use the crate like an external user.
- Retained source example: Unit test module
pub fn add(left: i32, right: i32) -> i32 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}
The cfg(test) module compiles only for test builds.
- Testing Rust Code: Beyond example tests (walkthrough, 53 min) — Property tests check broad invariants over generated inputs. Snapshot tests can protect structured output. Benchmarks and profiling should be used when performance claims matter.
- Retained source example: Property-style invariant
fn reverse_twice(input: &str) -> String {
input.chars().rev().collect::<String>().chars().rev().collect()
}
#[test]
fn reversing_twice_returns_original() {
let input = "rustacean";
assert_eq!(reverse_twice(input), input);
}
A property testing crate can generalize this idea over many generated strings.
- Testing Rust Code: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Test behavior and invariants rather than private implementation details when possible.
- Practice: Add regression tests before fixing subtle ownership, parsing, or concurrency bugs.
- Practice: Run clippy on all targets so examples, tests, and benches are linted too.
- Avoid: Only testing happy paths.
- Avoid: Putting broad integration scenarios inside private unit tests.
- Avoid: Letting snapshot tests become unreviewed bulk updates.
- Testing Rust Code: references (review, 2 min) — Original references retained from the legacy library.
- Rust book: testing: https://doc.rust-lang.org/book/ch11-00-testing.html
- proptest: https://docs.rs/proptest/latest/proptest/
- criterion: https://docs.rs/criterion/latest/criterion/
Code example
Rust in src/bin/jobctl.rs.
#[derive(clap::Parser, Debug)]
struct Args {
#[arg(long, env = "SERVICE_URL")]
service_url: String,
#[arg(long, default_value_t = 30)]
timeout_seconds: u64,
}
Walkthrough examples
- CLI, Async Persistence, and Release in a job orchestration service — A team is extending an Axum service with a companion CLI and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: src/bin/jobctl.rs
- File: tests/cli-async-persistence-and-release.spec
- File: docs/rust-systems/cli-async-persistence-and-release.md
- Start from the provided Rust snippet and make the intent visible in names and boundaries.
- Apply the checklist item "CLI has help output" before adding extra behavior.
- Write down how the implementation changes when tokio fails or becomes slow.
- Tokio entry point — The tokio::main macro creates a runtime and blocks the main thread on the async main future.
- Retained source code:
#[tokio::main]
async fn main() {
let body = fetch_status().await;
println!("{body}");
}
async fn fetch_status() -> &'static str {
"ok"
}
- Expected output: ok
- Compare the example with the canonical PTLearn implementation.
- Timeout around async work — timeout returns an error if the inner future does not complete before the duration.
- Retained source code:
use std::time::Duration;
use tokio::time::timeout;
async fn call_service() -> Result<String, &'static str> {
let result = timeout(Duration::from_secs(2), async {
"response".to_string()
})
.await
.map_err(|_| "service timed out")?;
Ok(result)
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Spawning independent work — Awaiting a JoinHandle observes task completion and captures panics as JoinError.
- Retained source code:
async fn run_jobs() -> Result<(), tokio::task::JoinError> {
let handle = tokio::spawn(async {
21 * 2
});
let answer = handle.await?;
assert_eq!(answer, 42);
Ok(())
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Unit test module — The cfg(test) module compiles only for test builds.
- Retained source code:
pub fn add(left: i32, right: i32) -> i32 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Property-style invariant — A property testing crate can generalize this idea over many generated strings.
- Retained source code:
fn reverse_twice(input: &str) -> String {
input.chars().rev().collect::<String>().chars().rev().collect()
}
#[test]
fn reversing_twice_returns_original() {
let input = "rustacean";
assert_eq!(reverse_twice(input), input);
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- CI quality gate — These commands catch formatting drift, lint regressions, and test failures.
- Retained source code:
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Add a submit command to a CLI.
- Store jobs through a repository trait.
- Run cargo fmt, clippy, and test before release.
- Diagnose async blocking: A Tokio request handler calls std::thread::sleep for two seconds. Name the issue and the preferred async alternative. The guided runner checks deterministic text.
- Starter code: issue:
alternative:
- Hint: std::thread::sleep blocks an OS thread.
- Hint: Tokio has an async timer module.
- Reference solution: issue: blocking the async runtime worker thread
alternative: tokio::time::sleep(...).await
- Accepted answers: blocking | tokio::time::sleep
- Select a test type: Choose unit test, integration test, or doc test for: private helper edge cases, public HTTP client flow, example in API documentation. The guided runner checks deterministic terms.
- Starter code: private helper:
public flow:
documentation example:
- Hint: Private helper tests usually sit near implementation.
- Hint: Integration tests call public APIs.
- Hint: Doc tests live in documentation comments.
- Reference solution: private helper: unit test
public flow: integration test
documentation example: doc test
- Accepted answers: unit test | integration test | doc test
Checklist
- CLI has help output
- Async database calls use shared state
- Background tasks observe shutdown
- Release commands are documented
Quiz prompts
- Why share request and response structs between a Rust service and CLI when practical? — Shared models reduce drift between the service API and companion tooling.
- A teammate wants to hide clap inside a convenient helper. What should you check first? — Place clap at the boundary that keeps job orchestration service behavior explicit, testable, and reviewable.
- Which artifact best proves this Rust Systems lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Intermediate async tooling: a teammate says the happy path works, but "CLI contract" is still implicit. What should you ask for before merging? — CLI contract belongs in the intermediate stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this intermediate Rust Systems slice. Which evidence is strongest? — Ship an intermediate Rust tool/service slice with CLI help output, async persistence boundary, and release-check evidence.
- What happens when you call an async function without awaiting or spawning its future? — Futures are lazy; an executor must poll them for work to happen.
- What is the main advantage of integration tests in tests/ for a library crate? — Integration tests depend on the crate from the outside, which catches public API and wiring issues.
Flashcards
- Intermediate async tooling: what decision does "CLI contract" force you to make? Make flags, help text, exit codes, and output formats stable enough for automation. Evidence prompt: Add one CLI command and snapshot the help output.
- Intermediate async tooling: what decision does "Async persistence boundary" force you to make? Keep database calls behind async functions that return typed domain results. Evidence prompt: Move one persistence call behind an async repository function and test the error path.
- Intermediate async tooling: what decision does "Release check script" force you to make? Run format, lint, tests, and package checks as one repeatable release command. Evidence prompt: Create a release command log with expected passing output.
- In Rust Systems, what should you remember about clap? clap matters here because it supports "Parse typed command-line arguments.".
- In Rust Systems, what should you remember about tokio? tokio matters here because it supports "Use async repositories behind application state.".
- In Rust Systems, what should you remember about sqlx pool? sqlx pool matters here because it supports "Run fmt, clippy, tests, and documented release commands.".
- In Rust Systems, what should you remember about cargo clippy? cargo clippy matters here because it supports "Parse typed command-line arguments.".
Labs
- Ship a cli, async persistence, and release slice — Extend an Axum service with a companion CLI with a small but reviewable feature that proves the lesson's architecture in code.
- Ship an intermediate Rust tool/service slice with CLI help output, async persistence boundary, and release-check evidence.
- Add one CLI command and snapshot the help output.
- Move one persistence call behind an async repository function and test the error path.
- Create a release command log with expected passing output.
- Add a submit command to a CLI.
- Store jobs through a repository trait.
- The lab demonstrates the intermediate async tooling outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: CLI contract, Async persistence boundary, Release check script.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready cli, async persistence, and release (Core) — Ship an intermediate Rust tool/service slice with CLI help output, async persistence boundary, and release-check evidence.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from src/bin/jobctl.rs plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL