Crate
Crate
Level 1 — Foundations A compilation unit in Rust; either a binary (executable) or a library.
1. Prerequisites
- Cargo — Rust's build system and package manager that creates, builds, and manages crates
2. Term Category
Rust Compilation Unit (the compilation & distribution atom)
While every language has some concept of a "compilation unit" (Java has JARs, Go has packages, C has translation units), the crate as Rust defines it — a single rooted tree of modules compiled as one atomic unit, with explicit visibility boundaries — is a Rust-specific design. The name itself, "crate," is unique to the Rust ecosystem.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Here's the problem we were staring at.
In C and C++, the "compilation unit" is a single .c or .cpp file. Each file is compiled independently, and the linker stitches them together at the end. This means the compiler can't see across files during compilation — it can't check types across boundaries, can't optimize globally, and can't enforce visibility rules. Header files attempt to bridge this gap, but they're fragile, duplicated, and a constant source of bugs (include order matters, missing guards cause redefinitions, macros leak everywhere).
We wanted something different. We wanted a compilation unit that was big enough to let the compiler see the whole picture — all the types, all the functions, all the visibility rules — in one shot. But not so big that it became unwieldy. We needed a Goldilocks unit.
That's the crate.
A crate is the largest unit the Rust compiler processes at once. It starts from a single root file (src/main.rs for binaries, src/lib.rs for libraries) and includes everything reachable through mod declarations. The compiler sees the entire crate as one coherent unit, which means it can:
- Enforce privacy: items are private by default, and only
pubitems cross crate boundaries - Check types end-to-end: no header file mismatches, no "undefined reference" surprises
- Optimize globally: inlining, monomorphization, and dead code elimination happen within the whole crate
We also made a deliberate choice: there are exactly two kinds of crates — binary crates (which produce executables) and library crates (which produce reusable code). No ambiguity, no special configuration. A binary crate has a main() function. A library crate doesn't. That's it.
The name "crate" itself was a playful nod — Cargo ships crates. It stuck.
(2) Reality Metaphor
Think of a crate as a shipping container.
A shipping container is a standard-sized box that holds a complete shipment. Everything inside it is organized, labeled, and sealed as a single unit. When the container arrives at a port (your project), you don't need to know how things are arranged inside — you just know what the container exports (its public interface).
There are two types of containers:
- A delivery container (binary crate) — it arrives at its destination and its contents are used directly (you run the executable)
- A supply container (library crate) — its contents are unpacked and assembled into something else (other crates depend on it)
The port authority (Cargo) manages loading, transporting, and unloading these containers. And the container's walls enforce boundaries — you can't reach into someone else's container and grab a private item without permission.
(3) Rust Code Examples
Short Snippet — Two kinds of crates
# Create a binary crate (has src/main.rs with fn main())
cargo new my_app
# Create a library crate (has src/lib.rs, no main function)
cargo new my_lib --lib
// src/main.rs — the root of a BINARY crate
fn main() {
println!("I'm a binary crate — I run as a program!");
}
// src/lib.rs — the root of a LIBRARY crate
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
Fuller Example — A library crate used by a binary crate
First, create the library crate:
// my_math/src/lib.rs — a library crate providing math utilities
/// Adds two numbers together.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Multiplies two numbers together.
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
// This function is PRIVATE — only usable inside this crate
fn internal_helper() -> i32 {
42
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_multiply() {
assert_eq!(multiply(4, 5), 20);
}
#[test]
fn test_internal_helper() {
// We CAN access private functions within the same crate
assert_eq!(internal_helper(), 42);
}
}
Then, use it from a binary crate:
# my_app/Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2024"
[dependencies]
my_math = { path = "../my_math" } # Depend on our local library crate
// my_app/src/main.rs — a binary crate consuming the library
use my_math::{add, multiply}; // Import public items from the library crate
fn main() {
let sum = add(10, 20);
let product = multiply(5, 6);
println!("10 + 20 = {}", sum); // 10 + 20 = 30
println!("5 × 6 = {}", product); // 5 × 6 = 30
// my_math::internal_helper(); // ❌ ERROR: this function is private!
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Crate Scoping and Lifecycle Rules
The mistake: Assuming Crate instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("crate_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("crate_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Crate State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Crate through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Crate Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Crate instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: High-Frequency Order Matching Engine Crate Architecture & Public Facades
Scenario:
You are architecting a high-frequency trading (HFT) order matching engine library crate named matching_engine. In financial execution systems, core domain invariants — such as price-time priority, non-zero order quantities, strictly ordered bid/ask depth, and atomic trade fill reports — must be guaranteed at the compilation boundary. Exposing internal data structures directly allows downstream callers or binary crates to corrupt book state or bypass compliance risk checks.
You must design a multi-module library crate structure that encapsulates internal order storage and matching logic within pub(crate) visibility while exposing a clean public API facade (Engine, Order, Side, ExecutionReport, OrderBookError) at the crate root via pub use re-exports.
Requirements:
- Define a
matching_enginecrate structure containing submodules:types,orderbook, andmatching. - Expose core public domain models in
types:Order(fields:id: u64,side: Side,price: u64,quantity: u32),Side(Buy/Sell),ExecutionReport, andOrderBookError. - Encapsulate
OrderBookinsideorderbookwithpub(crate)visibility, preventing external binary crates from mutating internalbidsorasksarrays. - Encapsulate
match_ordersinsidematchingwithpub(crate)visibility. Matching logic must match bids (descending price) against asks (ascending price) wheneverbid.price >= ask.price, executing fills at the maker's (ask) price. - Provide a public facade struct
Enginewithsubmit_order(&mut self, order: Order) -> Result<Vec<ExecutionReport>, OrderBookError>. Reject orders with zero price or zero quantity withErr(OrderBookError::InvalidPriceOrQuantity). - Write complete, compilable Rust code with comprehensive unit tests (
#[cfg(test)] mod tests) featuring explicit assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
pub mod matching_engine {
pub mod types {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Side {
Buy,
Sell,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Order {
pub id: u64,
pub side: Side,
pub price: u64, // Price in cents
pub quantity: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecutionReport {
pub maker_order_id: u64,
pub taker_order_id: u64,
pub executed_price: u64,
pub executed_quantity: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderBookError {
InvalidPriceOrQuantity,
OrderNotFound(u64),
}
}
mod orderbook {
use super::types::{Order, OrderBookError, Side};
#[derive(Debug, Default)]
pub(crate) struct OrderBook {
pub(crate) bids: Vec<Order>, // Sorted descending by price
pub(crate) asks: Vec<Order>, // Sorted ascending by price
}
impl OrderBook {
pub(crate) fn new() -> Self {
Self {
bids: Vec::new(),
asks: Vec::new(),
}
}
pub(crate) fn insert(&mut self, order: Order) -> Result<(), OrderBookError> {
if order.price == 0 || order.quantity == 0 {
return Err(OrderBookError::InvalidPriceOrQuantity);
}
match order.side {
Side::Buy => {
self.bids.push(order);
self.bids.sort_by(|a, b| b.price.cmp(&a.price));
}
Side::Sell => {
self.asks.push(order);
self.asks.sort_by(|a, b| a.price.cmp(&b.price));
}
}
Ok(())
}
}
}
mod matching {
use super::orderbook::OrderBook;
use super::types::ExecutionReport;
use std::cmp::min;
pub(crate) fn match_orders(book: &mut OrderBook) -> Vec<ExecutionReport> {
let mut reports = Vec::new();
while !book.bids.is_empty() && !book.asks.is_empty() {
let best_bid_price = book.bids[0].price;
let best_ask_price = book.asks[0].price;
if best_bid_price >= best_ask_price {
let fill_qty = min(book.bids[0].quantity, book.asks[0].quantity);
let exec_price = book.asks[0].price; // Maker price execution
reports.push(ExecutionReport {
maker_order_id: book.asks[0].id,
taker_order_id: book.bids[0].id,
executed_price: exec_price,
executed_quantity: fill_qty,
});
book.bids[0].quantity -= fill_qty;
book.asks[0].quantity -= fill_qty;
if book.bids[0].quantity == 0 {
book.bids.remove(0);
}
if book.asks[0].quantity == 0 {
book.asks.remove(0);
}
} else {
break;
}
}
reports
}
}
// Public crate root re-exports
pub use types::{ExecutionReport, Order, OrderBookError, Side};
#[derive(Debug, Default)]
pub struct Engine {
book: orderbook::OrderBook,
}
impl Engine {
pub fn new() -> Self {
Self {
book: orderbook::OrderBook::new(),
}
}
pub fn submit_order(&mut self, order: Order) -> Result<Vec<ExecutionReport>, OrderBookError> {
self.book.insert(order)?;
let reports = matching::match_orders(&mut self.book);
Ok(reports)
}
pub fn active_bids_count(&self) -> usize {
self.book.bids.len()
}
pub fn active_asks_count(&self) -> usize {
self.book.asks.len()
}
}
}
#[cfg(test)]
mod tests {
use super::matching_engine::*;
#[test]
fn test_order_submission_and_matching() {
let mut engine = Engine::new();
let sell_order = Order {
id: 101,
side: Side::Sell,
price: 100,
quantity: 10,
};
let res = engine.submit_order(sell_order);
assert!(res.is_ok());
let reports = res.unwrap();
assert!(reports.is_empty());
assert_eq!(engine.active_asks_count(), 1);
let buy_order = Order {
id: 102,
side: Side::Buy,
price: 105,
quantity: 5,
};
let res_buy = engine.submit_order(buy_order);
assert!(res_buy.is_ok());
let reports = res_buy.unwrap();
assert_eq!(reports.len(), 1);
let report = &reports[0];
assert_eq!(report.maker_order_id, 101);
assert_eq!(report.taker_order_id, 102);
assert_eq!(report.executed_price, 100);
assert_eq!(report.executed_quantity, 5);
assert_eq!(engine.active_asks_count(), 1);
assert_eq!(engine.active_bids_count(), 0);
}
#[test]
fn test_invalid_order_rejection() {
let mut engine = Engine::new();
let invalid_order = Order {
id: 1,
side: Side::Buy,
price: 0,
quantity: 10,
};
let result = engine.submit_order(invalid_order);
assert!(result.is_err());
assert!(matches!(result, Err(OrderBookError::InvalidPriceOrQuantity)));
assert_ne!(engine.active_bids_count(), 1);
}
}
Technical Explanation
- Compilation Unit Boundaries & Visibility Controls: In Rust, visibility is enforced at compile time per crate compilation unit. By marking
OrderBookfields andmatch_ordersaspub(crate), they are accessible across all submodules insidematching_enginebut completely hidden from external consumer binary crates. This prevents caller code from arbitrarily inserting unordered orders or modifying queued quantities. - Re-export Facades (
pub use): Usingpub use types::{...}at the crate root enables crate consumers to writeuse matching_engine::{Engine, Order, Side}without leaking the internal directory structure (matching_engine::types::Order). - Ownership and Mutability Invariants:
submit_order(&mut self, order: Order)takes ownership of the incomingOrderstruct, preventing callers from retaining a mutable pointer to an order once queued. Exclusive mutable borrowing (&mut self) guarantees single-threaded state mutation safety without requiring runtime lock overhead. - Edge Cases:
-
Zero price or quantity: Validated during insertion and rejected immediately.
-
Partial fills: Quantities are updated in place, and fully filled orders are removed from book heads (
bids[0]/asks[0]). -
Unmatched orders: Order remains cleanly sorted in bid/ask depth for future incoming takers.
-
Exercise 2: Embedded Telemetry Protocol Crate with Zero-Allocation Binary Framing & Feature-Gated Conditional Compilation
Scenario:
In an embedded IoT gateway architecture, your library crate telemetry_codec must format and decode binary sensor telemetry packets under strict memory constraints. The crate must operate across two compilation environments:
- Microcontroller embedded targets: Operates with zero heap allocations, stack-allocated 8-byte binary frames, and CRC-16-CCITT payload checksum validation.
- Host diagnostic targets: Features rich string output formatting controlled via crate feature flags (
#[cfg(feature = "std_diagnostics")]).
You must architect the binary framing logic and feature gates to maintain compile-time safety and memory efficiency across targets.
Requirements:
- Define binary frame layout (8 bytes, Big-Endian):
- Bytes 0..2:
sensor_id(u16) - Bytes 2..6:
value(u32) - Bytes 6..8:
checksum(u16computed over bytes 0..6 via CRC-16 CCITT polynomial0x1021, seed0xFFFF).
- Bytes 0..2:
- Implement
TelemetryFrame::encode(&self) -> [u8; 8]returning a stack-allocated byte array without heap allocation. - Implement
TelemetryFrame::decode(bytes: &[u8]) -> Result<Self, CodecError>. ReturnCodecError::BufferTooSmallifbytes.len() < 8,CodecError::InvalidSensorIdifsensor_id == 0, andCodecError::ChecksumMismatch { expected, actual }if CRC verification fails. - Add feature-gated
format_diagnostic(&self) -> Stringcompiled only when#[cfg(feature = "std_diagnostics")]is active. - Provide complete, compilable Rust code with a full test module (
#[cfg(test)] mod tests) with explicit assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
pub mod telemetry_codec {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TelemetryFrame {
pub sensor_id: u16,
pub value: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecError {
BufferTooSmall,
InvalidSensorId,
ChecksumMismatch { expected: u16, actual: u16 },
}
/// Computes CRC-16-CCITT over a byte slice.
pub fn calculate_crc16(bytes: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &byte in bytes {
crc ^= (byte as u16) << 8;
for _ in 0..8 {
if (crc & 0x8000) != 0 {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
crc
}
impl TelemetryFrame {
pub fn new(sensor_id: u16, value: u32) -> Result<Self, CodecError> {
if sensor_id == 0 {
return Err(CodecError::InvalidSensorId);
}
Ok(Self { sensor_id, value })
}
pub fn encode(&self) -> [u8; 8] {
let mut buf = [0u8; 8];
buf[0..2].copy_from_slice(&self.sensor_id.to_be_bytes());
buf[2..6].copy_from_slice(&self.value.to_be_bytes());
let crc = calculate_crc16(&buf[0..6]);
buf[6..8].copy_from_slice(&crc.to_be_bytes());
buf
}
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
if bytes.len() < 8 {
return Err(CodecError::BufferTooSmall);
}
let sensor_id = u16::from_be_bytes([bytes[0], bytes[1]]);
if sensor_id == 0 {
return Err(CodecError::InvalidSensorId);
}
let value = u32::from_be_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]);
let expected_crc = u16::from_be_bytes([bytes[6], bytes[7]]);
let actual_crc = calculate_crc16(&bytes[0..6]);
if expected_crc != actual_crc {
return Err(CodecError::ChecksumMismatch {
expected: expected_crc,
actual: actual_crc,
});
}
Ok(Self { sensor_id, value })
}
#[cfg(feature = "std_diagnostics")]
pub fn format_diagnostic(&self) -> String {
format!("[SENSOR 0x{:04X}] Value: {}", self.sensor_id, self.value)
}
}
}
#[cfg(test)]
mod tests {
use super::telemetry_codec::*;
#[test]
fn test_encode_decode_roundtrip() {
let frame = TelemetryFrame::new(0x10A, 42949).unwrap();
let encoded = frame.encode();
assert_eq!(encoded.len(), 8);
let decoded = TelemetryFrame::decode(&encoded);
assert!(decoded.is_ok());
let decoded_frame = decoded.unwrap();
assert_eq!(decoded_frame, frame);
assert_eq!(decoded_frame.sensor_id, 0x10A);
assert_eq!(decoded_frame.value, 42949);
}
#[test]
fn test_checksum_mismatch_detection() {
let frame = TelemetryFrame::new(0x20, 100).unwrap();
let mut encoded = frame.encode();
// Corrupt payload byte
encoded[3] ^= 0xFF;
let decoded = TelemetryFrame::decode(&encoded);
assert!(decoded.is_err());
assert!(matches!(decoded, Err(CodecError::ChecksumMismatch { .. })));
assert_ne!(decoded, Ok(frame));
}
#[test]
fn test_invalid_sensor_and_short_buffer() {
let invalid_frame = TelemetryFrame::new(0, 50);
assert!(invalid_frame.is_err());
assert!(matches!(invalid_frame, Err(CodecError::InvalidSensorId)));
let short_buf = [0u8; 5];
let decoded_short = TelemetryFrame::decode(&short_buf);
assert_eq!(decoded_short, Err(CodecError::BufferTooSmall));
}
}
Technical Explanation
- Zero-Allocation Stack Guarantees: Stack arrays
[u8; 8]avoid heap allocations (Vec<u8>), making the crate compatible withno_stdembedded microcontrollers. Operations likeu16::from_be_bytesandu32::to_be_byteslower directly to byte-swapping instructions without memory overhead. - Conditional Compilation Gates (
#[cfg]): Crate feature flags operate at the compilation unit level. Items annotated with#[cfg(feature = "std_diagnostics")]are completely omitted from the AST when the feature is disabled, guaranteeing zero code bloat or standard library link dependencies on embedded targets. - CRC Invariants and Data Alignment: Calculating the CCITT CRC-16 over slice
&bytes[0..6]ensures data integrity against payload corruption over lossy bus lines (RS-485 / CAN bus). - Edge Cases:
-
Truncated buffers (< 8 bytes) trigger an immediate early return of
BufferTooSmall. -
Reserved
sensor_id == 0is guarded against invalid telemetry source initialization. -
Corrupted payload bytes fail CRC verification and trigger
ChecksumMismatch.
-
Exercise 3: Plugin-Driven Async Task Pipeline Engine with Shared Library Crate Facade & Dynamic Dispatch
Scenario:
You are building an enterprise pipeline orchestrator library crate task_pipeline. The library crate defines a composable pipeline where distinct processing steps (e.g. data validation, metrics logging, transformation) can be registered dynamically by consuming binary crates. Each task implements a PipelineTask trait and is executed under a configurable RetryPolicy.
Architect a clean separation between the library crate facade and binary task dispatchers, providing type-safe context mutation, dynamic trait dispatch (Box<dyn PipelineTask>), and consolidated execution reports.
Requirements:
- Define trait
PipelineTask:fn name(&self) -> &str;fn execute(&self, ctx: &mut TaskContext) -> Result<TaskOutput, TaskError>;
- Define
TaskContextholding state (HashMap<String, String>) and execution attempt counter. - Define
RetryPolicy:NoRetryorMaxRetries(u32). - Build
Pipelinestruct allowing tasks to be appended viaadd_task(&mut self, task: Box<dyn PipelineTask>, policy: RetryPolicy). - Implement
Pipeline::run(&mut self, ctx: &mut TaskContext) -> PipelineReport. If a task fails, retry according to its policy up tomax_retries. Aggregate total processed bytes and task status metrics. - Provide complete compilable code and unit tests (
#[cfg(test)] mod tests) with explicit assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
pub mod task_pipeline {
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaskOutput {
pub message: String,
pub bytes_processed: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TaskError {
ExecutionFailed(String),
InvalidContext(String),
Timeout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryPolicy {
NoRetry,
MaxRetries(u32),
}
#[derive(Debug, Default)]
pub struct TaskContext {
state: HashMap<String, String>,
execution_count: u32,
}
impl TaskContext {
pub fn new() -> Self {
Self {
state: HashMap::new(),
execution_count: 0,
}
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.state.insert(key.into(), value.into());
}
pub fn get(&self, key: &str) -> Option<&str> {
self.state.get(key).map(|s| s.as_str())
}
pub fn increment_executions(&mut self) {
self.execution_count += 1;
}
pub fn execution_count(&self) -> u32 {
self.execution_count
}
}
pub trait PipelineTask {
fn name(&self) -> &str;
fn execute(&self, ctx: &mut TaskContext) -> Result<TaskOutput, TaskError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PipelineReport {
pub total_tasks: usize,
pub successful_tasks: usize,
pub failed_tasks: usize,
pub total_bytes: usize,
}
pub struct Pipeline {
tasks: Vec<(Box<dyn PipelineTask>, RetryPolicy)>,
}
impl Pipeline {
pub fn new() -> Self {
Self { tasks: Vec::new() }
}
pub fn add_task(&mut self, task: Box<dyn PipelineTask>, policy: RetryPolicy) {
self.tasks.push((task, policy));
}
pub fn run(&mut self, ctx: &mut TaskContext) -> PipelineReport {
let mut successful = 0;
let mut failed = 0;
let mut total_bytes = 0;
for (task, policy) in &self.tasks {
let mut attempts = 0;
let max_attempts = match policy {
RetryPolicy::NoRetry => 1,
RetryPolicy::MaxRetries(n) => *n + 1,
};
let mut task_success = false;
while attempts < max_attempts {
attempts += 1;
ctx.increment_executions();
match task.execute(ctx) {
Ok(output) => {
total_bytes += output.bytes_processed;
task_success = true;
break;
}
Err(_err) => {
// Retry on failure if attempts remain
}
}
}
if task_success {
successful += 1;
} else {
failed += 1;
}
}
PipelineReport {
total_tasks: self.tasks.len(),
successful_tasks: successful,
failed_tasks: failed,
total_bytes,
}
}
}
}
#[cfg(test)]
mod tests {
use super::task_pipeline::*;
struct MockTransformTask {
name: String,
should_fail_times: std::cell::RefCell<u32>,
}
impl MockTransformTask {
fn new(name: &str, fail_times: u32) -> Self {
Self {
name: name.to_string(),
should_fail_times: std::cell::RefCell::new(fail_times),
}
}
}
impl PipelineTask for MockTransformTask {
fn name(&self) -> &str {
&self.name
}
fn execute(&self, ctx: &mut TaskContext) -> Result<TaskOutput, TaskError> {
let mut fail_count = self.should_fail_times.borrow_mut();
if *fail_count > 0 {
*fail_count -= 1;
Err(TaskError::ExecutionFailed("Transient error".to_string()))
} else {
ctx.set("last_task", self.name());
Ok(TaskOutput {
message: "Success".to_string(),
bytes_processed: 1024,
})
}
}
}
#[test]
fn test_pipeline_execution_with_retry() {
let mut ctx = TaskContext::new();
let mut pipeline = Pipeline::new();
let task1 = Box::new(MockTransformTask::new("task1", 2));
pipeline.add_task(task1, RetryPolicy::MaxRetries(2));
let report = pipeline.run(&mut ctx);
assert_eq!(report.total_tasks, 1);
assert_eq!(report.successful_tasks, 1);
assert_eq!(report.failed_tasks, 0);
assert_eq!(report.total_bytes, 1024);
assert_eq!(ctx.execution_count(), 3);
assert_eq!(ctx.get("last_task"), Some("task1"));
}
#[test]
fn test_pipeline_task_exhaustion_failure() {
let mut ctx = TaskContext::new();
let mut pipeline = Pipeline::new();
let task1 = Box::new(MockTransformTask::new("failing_task", 5));
pipeline.add_task(task1, RetryPolicy::MaxRetries(1));
let report = pipeline.run(&mut ctx);
assert_eq!(report.total_tasks, 1);
assert_eq!(report.successful_tasks, 0);
assert_eq!(report.failed_tasks, 1);
assert_eq!(report.total_bytes, 0);
assert_eq!(ctx.execution_count(), 2);
assert_ne!(ctx.get("last_task"), Some("failing_task"));
assert!(matches!(report, PipelineReport { failed_tasks: 1, .. }));
}
}
Technical Explanation
- Dynamic Dispatch Across Crate Boundaries (
dyn Trait): By acceptingBox<dyn PipelineTask>, thetask_pipelinelibrary crate allows consuming binary crates to pass custom structs that implementPipelineTaskwithout requiring compile-time monomorphization or leaking concrete task types into the pipeline definition. - Context Mutability and Lifetime Invariants:
TaskContextis passed as an exclusive mutable reference (&mut TaskContext) down the pipeline chain. This enforces strict sequential ordering of state modifications without runtime lock overhead (Mutex/RwLock). - Retry Loop Mechanics: The execution loop tracks attempt counters per step. If a task returns
Err(TaskError), the pipeline checksRetryPolicy. Upon exhausting attempts, the failure is recorded inPipelineReport.failed_tasksand pipeline execution proceeds to the next stage. - Edge Cases:
-
Empty pipeline returns zeroed
PipelineReport. -
Multi-attempt recovery correctly updates total executions on
TaskContextwhile incrementingsuccessful_tasks. -
Context key overwrite replaces prior context values cleanly.
-
6. Related Terms
- Cargo — the tool that builds, tests, and manages crates
- Package — a Cargo concept wrapping one or more crates with a
Cargo.toml - Module — the organizational unit within a crate; crates contain modules
pubVisibility — controls what items are exposed beyond the crate boundaryCargo.toml— the manifest file that defines how a crate is built and its dependencies- Workspace — Related concept: Workspace.
- Integration Tests — Related concept: Integration Tests.
- Link-Time Optimization (LTO) — Related concept: Link-Time Optimization (LTO).
7. Key Takeaways
- A crate is Rust's compilation unit — the largest chunk of code the compiler processes at once, starting from a single root file.
- Two kinds, no ambiguity — binary crates (
src/main.rs, havefn main(), produce executables) and library crates (src/lib.rs, produce reusable code). - Crate ≠ package — a package (defined by
Cargo.toml) can contain multiple crates, but each crate is a single compilation unit. - Privacy is enforced at the crate boundary — items are private by default and only
pubitems are visible to other crates. - The crate root is the entry point —
src/main.rsorsrc/lib.rsis where the compiler starts; all modules branch out from there viamoddeclarations.