Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Composition and Concurrency

Traits, layers, events, and futures let libraries add behavior without owning the entire application.

RayonTowerTracingTokio

Rayon: preserve iterator vocabulary, change execution

Consumer goal. Compute independent results in parallel without manually creating threads, queues, or result slots.

use rayon::prelude::*;

fn score(number: u64) -> u64 {
    (0..number).map(|value| value * value).sum()
}

fn main() {
    let inputs = [10, 20, 30, 40];

    let scores: Vec<_> = inputs
        .par_iter()
        .map(|&number| score(number))
        .collect();

    println!("{scores:?}");
}

Observed stdout.

[285, 2470, 8555, 20540]

Boundary behavior. Work scheduling is nondeterministic, but this indexed parallel iterator preserves input order when collecting results.

Compile-time guarantee. Closure and item types must satisfy the thread-safety bounds required for parallel execution; unsuitable captured state is rejected.

Runtime boundary. Parallelism may be slower for small work, and panics or resource contention remain dynamic concerns.

Tradeoff. Familiar iterator vocabulary lowers adoption cost, but the small syntactic change can conceal a major execution and performance change.

Deep-dive question. How do extension traits and iterator plumbing preserve ordering while Rayon divides work?

Tower: add policy around an operation

Consumer goal. Apply a timeout to asynchronous business logic without putting timeout code inside the operation.

use std::{convert::Infallible, time::Duration};
use tower::{service_fn, ServiceBuilder, ServiceExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let greet = service_fn(|name: String| async move {
        Ok::<_, Infallible>(format!("Hello, {name}!"))
    });

    let service = ServiceBuilder::new()
        .timeout(Duration::from_secs(1))
        .service(greet);

    let response = service.oneshot("Alice".to_owned()).await?;
    println!("{response}");
    Ok(())
}

Observed stdout.

Hello, Alice!

Boundary behavior. If the service exceeds the deadline, the timeout layer returns its error instead of a response.

Compile-time guarantee. Layer composition constructs a concrete service type whose request, response, and error relationships must line up.

Runtime boundary. Readiness, timeouts, retry safety, and backpressure depend on actual load and operation semantics.

Tradeoff. One reusable Service abstraction unlocks an ecosystem of middleware, but nested generic types and error adaptation can become difficult to read.

Deep-dive question. How do Service, Layer, and ServiceBuilder separate business behavior from reusable policy?

Tracing: separate instrumentation from collection

Consumer goal. Attach typed context to an operation while allowing the application—not the library—to choose formatting and destination.

use tracing::{info, instrument};

#[instrument(fields(user_id))]
fn load_user(user_id: u64) {
    info!(cached = false, "loading user");
}

fn main() {
    tracing_subscriber::fmt()
        .with_target(false)
        .without_time()
        .with_ansi(false)
        .with_writer(std::io::stdout)
        .init();

    load_user(42);
}

Observed stdout with Tracing 0.1.44 and Tracing Subscriber 0.3.23.

 INFO load_user{user_id=42}: loading user cached=false

Boundary behavior. Without an installed subscriber, events may be ignored. Filtering and formatting are application policy.

Compile-time guarantee. Field expressions are ordinary typed Rust values; the macro records structure rather than requiring one preformatted string.

Runtime boundary. Subscriber configuration determines cost, visibility, format, and destination.

Tradeoff. Decoupling makes libraries composable, but a new consumer may be confused when instrumentation produces no visible output by itself.

Deep-dive question. How do spans carry contextual fields across nested and asynchronous work without the instrumented library knowing the subscriber?

Tokio: race futures and cancel the losers

Consumer goal. Stop waiting for a long operation when a cancellation signal arrives first.

use std::time::Duration;
use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let (cancel, cancelled) = oneshot::channel::<()>();

    tokio::spawn(async move {
        tokio::time::sleep(Duration::from_millis(10)).await;
        let _ = cancel.send(());
    });

    tokio::select! {
        _ = tokio::time::sleep(Duration::from_secs(1)) => {
            println!("operation completed");
        }
        _ = cancelled => {
            println!("operation cancelled");
        }
    }
}

Observed stdout.

operation cancelled

Boundary behavior. The first ready branch runs. Losing futures are dropped; if several are ready simultaneously, branch selection follows the macro’s fairness policy unless configured otherwise.

Compile-time guarantee. Each branch handles the output type of its own future, and moved values obey ordinary ownership rules across spawned tasks.

Runtime boundary. Timing determines the winner. Dropping a future only constitutes safe cancellation when that future’s API documents cancellation safety.

Tradeoff. select! makes asynchronous control flow local and legible, but it places subtle responsibility on consumers to understand drop behavior.

Deep-dive question. What code does select! generate, and how do polling, pinning, fairness, and cancellation interact?