Skill v2.0.0
currentAutomated scan96/100name: toolchains-rust-core description: "Rust 2024 edition core patterns: idiomatic code, error handling, traits/generics, macros, async/concurrency, testing, and project architecture" version: "2.0.0" category: toolchains-rust tags: [rust, patterns, performance, minimalism, efficiency, safety, async, testing, architecture] effort: medium
Rust 2024 Core Patterns
Quick Start
# Cargo.toml - essential stack[dependencies]tokio = { version = "1", features = ["full"] }thiserror = "2"anyhow = "2"serde = { version = "1", features = ["derive"] }tracing = "0.1"tracing-subscriber = { version = "0.3", features = ["env-filter"] }[dev-dependencies]rstest = "0.22"proptest = "1"mockall = "0.13"insta = "1"criterion = { version = "0.5", features = ["html_reports"] }
cargo add tokio --features full && cargo add thiserror anyhow serde --features serde/derivecargo clippy -- -D warnings # enforce in CIcargo test # unit + integration + doctestscargo test --doc # doctests only
Idiomatic Patterns
- Builder pattern: Use consuming
selfmethods; finish withbuild() -> Result<T, E>for validated construction. - Newtype pattern:
struct UserId(u64)gives type safety at zero cost; addimpl From<u64> for UserId. - From/Into: Implement
From<T>only —Into<T>is blanket-provided; acceptimpl Into<T>in function params. - Derive by default:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]on domain types; addserdeonly when needed. - Type state: Encode state machines as generic marker types — invalid transitions become compile errors, not panics.
- Borrowed params: Accept
&strnot&String,&[T]not&Vec<T>,impl AsRef<Path>not&PathBuf. - Default for config:
Config { field: value, ..Default::default() }— audit all fields when adding new ones. - RAII guards: Use
Dropfor resource cleanup; wrap locks, file handles, and connections in guard types. - `Cow<'_, str>`: Return
Cow<'_, str>when sometimes borrowing, sometimes owning — avoids unnecessary clones. - Zero-cost abstractions: Iterator chains compile to identical (often better) machine code than manual loops.
- Stack vs heap: Default to stack; use
Boxonly for trait objects, recursive types, or large data escaping scope.
Error Handling
thiserror (libraries)
use thiserror::Error;#[derive(Error, Debug)]pub enum UserError {#[error("user {id} not found")]NotFound { id: u64 },#[error("invalid email: {0}")]InvalidEmail(String),#[error("database error")]Database(#[from] sqlx::Error),#[error("serialization failed")]Serialization(#[from] serde_json::Error),}
Rules:
- Libraries expose typed error enums — callers can pattern-match variants.
- Never use
StringorBox<dyn Error>in public library API. #[from]generatesFrom<SourceError>— use?to propagate automatically.#[source]exposes the underlying error viaError::source()without auto-From.
anyhow (applications / CLIs)
use anyhow::{Context, Result};fn load_config(path: &Path) -> Result<Config> {let text = std::fs::read_to_string(path).with_context(|| format!("reading config from {}", path.display()))?;let config: Config = toml::from_str(&text).context("parsing config as TOML")?;Ok(config)}fn main() -> Result<()> {let config = load_config(Path::new("config.toml"))?;run(config)}
Rules:
anyhow::Result<T>inmain(), CLI handlers, and top-level application code.- Chain context with
.context("what we were doing")at each call site. - Never use
anyhowin public library APIs — wrap and convert at the boundary.
Error conversion at library/app boundary
// In application code: convert library error to anyhowlet user = user_service.get(id).await.map_err(|e| anyhow::anyhow!("get user {id}: {e}"))?;// Or implement From in your app error typeimpl From<UserError> for AppError { ... }
Traits and Generics
Small, focused traits
// Prefer small traits over large catch-all interfacestrait Fetch {async fn fetch(&self, url: &str) -> Result<Bytes, FetchError>;}trait Store {async fn store(&self, key: &str, value: &[u8]) -> Result<(), StoreError>;}// Compose with supertraitstrait Cache: Fetch + Store + Send + Sync {}
Trait objects vs generics
// Generics: compile-time dispatch, monomorphized, no heap allocfn process<F: Fetch>(fetcher: &F) -> Result<()> { ... }// Trait objects: runtime dispatch, heap alloc, heterogeneous collectionsfn process_dyn(fetcher: &dyn Fetch) -> Result<()> { ... }fn process_arc(fetcher: Arc<dyn Fetch>) -> Result<()> { ... }// Rule: default to generics; use dyn Trait for:// - Heterogeneous collections (Vec<Box<dyn Trait>>)// - Runtime polymorphism (DI containers, plugin systems)// - Avoiding monomorphization bloat in large codebases
Associated types vs generics
// Associated type: one implementation per type (Iterator pattern)trait Parser {type Output;fn parse(&self, input: &str) -> Result<Self::Output, ParseError>;}// Generic parameter: multiple implementations per typetrait Convert<T> {fn convert(&self) -> T;}// allows: impl Convert<String> for MyType AND impl Convert<u64> for MyType
Generic bounds and where clauses
// Inline bounds for simple casesfn clone_and_display<T: Clone + Display>(value: &T) { ... }// Where clause for complex cases (preferred for readability)fn process<T, E>(items: &[T]) -> Result<Vec<String>, E>whereT: Display + Send + 'static,E: std::error::Error + From<fmt::Error>,{ ... }
Blanket implementations
// Provide default impl for all qualifying typestrait Summarize {fn summary(&self) -> String;}impl<T: Display> Summarize for T {fn summary(&self) -> String {format!("{}", self)}}
Extension traits (adding methods to foreign types)
trait IteratorExt: Iterator {fn take_while_inclusive<P>(self, predicate: P) -> TakeWhileInclusive<Self, P>whereP: FnMut(&Self::Item) -> bool,Self: Sized;}impl<I: Iterator> IteratorExt for I {fn take_while_inclusive<P>(self, predicate: P) -> TakeWhileInclusive<Self, P>where P: FnMut(&Self::Item) -> bool, Self: Sized {TakeWhileInclusive { iter: self, predicate, done: false }}}
Sealed traits (prevent external implementation)
mod private {pub trait Sealed {}}pub trait MyTrait: private::Sealed {fn method(&self);}// Only types you impl private::Sealed for can impl MyTraitimpl private::Sealed for MyType {}impl MyTrait for MyType { fn method(&self) {} }
Generic Associated Types (GATs)
// Stable since Rust 1.65 — enables lifetime-parameterized associated typestrait Lender {type Item<'a> where Self: 'a;fn lend(&mut self) -> Self::Item<'_>;}
Macros
Declarative macros (macro_rules!)
// Pattern matching on token treesmacro_rules! hashmap {($($key:expr => $val:expr),* $(,)?) => {{let mut map = std::collections::HashMap::new();$(map.insert($key, $val);)*map}};}let m = hashmap!{ "a" => 1, "b" => 2 };// Repeat patternsmacro_rules! impl_from_str {($($t:ty),+) => {$(impl std::str::FromStr for $t {type Err = ParseError;fn from_str(s: &str) -> Result<Self, Self::Err> {s.parse::<u64>().map(Self).map_err(Into::into)}})+};}
Procedural macros (derive)
// Custom derive — separate crate required (e.g., my-macros-derive)use proc_macro::TokenStream;use quote::quote;use syn::{parse_macro_input, DeriveInput};#[proc_macro_derive(Builder)]pub fn derive_builder(input: TokenStream) -> TokenStream {let ast = parse_macro_input!(input as DeriveInput);let name = &ast.ident;let expanded = quote! {impl #name {pub fn builder() -> #nameBuilder { Default::default() }}};expanded.into()}
Rules:
- Use declarative macros for simple repetition; proc macros for derive and attribute macros.
- Document macro hygiene:
$crate::prefix for items referenced inside the macro. - Prefer
derive_moreandstrumcrates over writing your own derive macros. - Test macros via
trybuildto verify compile-time error messages.
Async / Concurrency Patterns
Tokio fundamentals
#[tokio::main]async fn main() -> anyhow::Result<()> {// Tokio only — async-std is effectively unmaintained as of 2026run().await}// Async closures (Rust 2024)let handler = async |req: Request| -> Result<Response> {process(req).await};
Structured concurrency
use tokio::{join, select};// Concurrent execution (all complete)let (users, orders) = join!(fetch_users(), fetch_orders());// Racing (first to complete wins)select! {result = fetch_with_primary() => result,result = fetch_with_fallback() => result,_ = tokio::time::sleep(Duration::from_secs(5)) => Err(TimeoutError),}// Dynamic task groupsuse tokio::task::JoinSet;let mut set = JoinSet::new();for id in ids {set.spawn(async move { fetch_item(id).await });}while let Some(result) = set.join_next().await {handle(result??);}
Shared state patterns
// Arc<Mutex<T>> for mutable shared statelet state: Arc<Mutex<HashMap<u64, User>>> = Arc::new(Mutex::new(HashMap::new()));let state_clone = Arc::clone(&state);tokio::spawn(async move {let mut guard = state_clone.lock().await; // tokio::sync::Mutexguard.insert(1, user);});// RwLock for read-heavy workloadslet cache: Arc<RwLock<HashMap<String, Value>>> = Arc::new(RwLock::new(HashMap::new()));let val = cache.read().await.get("key").cloned();// Atomics for simple flags/counters — no mutex overheaduse std::sync::atomic::{AtomicU64, Ordering};let counter = Arc::new(AtomicU64::new(0));counter.fetch_add(1, Ordering::SeqCst);
Actor pattern (message-passing concurrency)
use tokio::sync::mpsc;enum Command {Get { key: String, reply: tokio::sync::oneshot::Sender<Option<String>> },Set { key: String, value: String },Shutdown,}struct CacheActor {store: HashMap<String, String>,rx: mpsc::Receiver<Command>,}impl CacheActor {async fn run(mut self) {while let Some(cmd) = self.rx.recv().await {match cmd {Command::Get { key, reply } => {let _ = reply.send(self.store.get(&key).cloned());}Command::Set { key, value } => {self.store.insert(key, value);}Command::Shutdown => break,}}}}// Handle is cheaply cloneable — clone per task/thread#[derive(Clone)]struct CacheHandle(mpsc::Sender<Command>);impl CacheHandle {async fn get(&self, key: String) -> Option<String> {let (tx, rx) = tokio::sync::oneshot::channel();self.0.send(Command::Get { key, reply: tx }).await.ok()?;rx.await.ok()?}async fn set(&self, key: String, value: String) {let _ = self.0.send(Command::Set { key, value }).await;}}
Cancellation safety
// RAII cleanup: use Drop for cancellation handlingstruct Guard {resource: Resource,}impl Drop for Guard {fn drop(&mut self) {// Always runs, even if task is cancelledself.resource.cleanup();}}// Avoid holding mutable state across .await points// BAD:let mut locked = mutex.lock().await;do_io().await; // lock held across await — deadlock riskdrop(locked);// GOOD:{let mut locked = mutex.lock().await;update_state(&mut locked);} // lock released before awaitdo_io().await;
CPU-bound work
// Never block the async executorlet result = tokio::task::spawn_blocking(|| {// CPU-intensive or blocking sync code hereheavy_computation()}).await?;// Rayon for data-parallel CPU workuse rayon::prelude::*;let result: Vec<_> = data.par_iter().map(|x| transform(x)).collect();
Async write durability
// Buffered tokio::File opened via OpenOptions MUST be flushed before a reader runs.let mut f = tokio::fs::OpenOptions::new().create(true).write(true).truncate(true).open(path).await?;f.write_all(&bytes).await?;f.flush().await?; // REQUIRED — without this, a concurrent reader may see truncated/empty data
Rules:
- After
tokio::fs::File::write_all(), callflush().awaitbefore any reader runs or you return — otherwise silent read-after-write race / data loss. tokio::fs::Filedefers writes to a blocking thread pool —flush().awaitensures all queued writes have completed before a reader runs (it is NOT a userspaceBufWriter). The free fntokio::fs::write()is a single open/write/close. Syncstd::fs::Filehas no Rust-layer userspace buffer, so writes reach the OS page cache on return — but crash/power-loss durability still requiressync_all()/sync_data(); drop alone does not fsync.
HTTP client construction
// Never reqwest::Client::new() in a service — no application-level timeout; a slow upstream can block a worker for the OS TCP timeout (minutes).let client = reqwest::ClientBuilder::new().timeout(Duration::from_secs(30)).connect_timeout(Duration::from_secs(5)).build()?;
Rules:
- Never construct
reqwest::Client::new()in a service — a timeout-free client has no application-level timeout, so a slow upstream can block a worker for the OS TCP timeout duration (minutes). - Always set both a request
timeoutand aconnect_timeout; build once and clone (the inner pool is shared).
Testing Best Practices
Test placement
// Unit tests: same file, cfg(test) module#[cfg(test)]mod tests {use super::*;#[test]fn test_basic_operation() {// Arrangelet sut = MyStruct::new(42);// Actlet result = sut.compute();// Assertassert_eq!(result, 84);}}// Integration tests: tests/ directory, no cfg(test) needed// tests/integration_test.rsuse my_crate::Api;#[tokio::test]async fn full_roundtrip() {let api = Api::start_test_server().await;// ...}
rstest — parametrized and fixture-based tests
use rstest::*;#[fixture]fn database() -> TestDb {TestDb::in_memory()}#[rstest]#[case("valid@email.com", true)]#[case("not-an-email", false)]#[case("", false)]fn test_email_validation(#[case] input: &str, #[case] expected: bool) {assert_eq!(is_valid_email(input), expected);}#[rstest]async fn test_with_fixture(database: TestDb) {let repo = UserRepository::new(database);assert!(repo.find(999).await.unwrap().is_none());}
proptest — property-based testing
use proptest::prelude::*;proptest! {#[test]fn roundtrip_serialization(s in "\\PC*") {let encoded = encode(&s);let decoded = decode(&encoded).unwrap();prop_assert_eq!(s, decoded);}#[test]fn sort_is_idempotent(mut v in prop::collection::vec(any::<i32>(), 0..100)) {v.sort();let sorted_once = v.clone();v.sort();prop_assert_eq!(sorted_once, v);}}
insta — snapshot testing
use insta::assert_snapshot;#[test]fn test_error_message() {let err = UserError::NotFound { id: 42 };assert_snapshot!(err.to_string());// First run writes snapshot; subsequent runs compare// Review with: cargo insta review}#[test]fn test_json_output() {let user = User { id: 1, name: "Alice".into() };insta::assert_json_snapshot!(user, @r###"{"id": 1,"name": "Alice"}"###);}
mockall — mock trait implementations
use mockall::{automock, predicate::*};#[cfg_attr(test, automock)]trait EmailService: Send + Sync {async fn send(&self, to: &str, body: &str) -> Result<(), EmailError>;}#[cfg(test)]mod tests {use super::*;#[tokio::test]async fn test_sends_welcome_email() {let mut mock = MockEmailService::new();mock.expect_send().with(eq("user@example.com"), contains("Welcome")).times(1).returning(|_, _| Ok(()));let service = UserService::new(Arc::new(mock));service.register("user@example.com").await.unwrap();}}
Async tests and multi-threaded scenarios
// Single-threaded (default)#[tokio::test]async fn test_basic_async() { ... }// Multi-threaded runtime for testing concurrent behavior#[tokio::test(flavor = "multi_thread", worker_threads = 4)]async fn test_concurrent_writes() { ... }
Doc tests
/// Parses a user ID from a string.////// # Examples////// ```/// use my_crate::UserId;////// let id = UserId::parse("42").unwrap();/// assert_eq!(id.value(), 42);/// ```////// ```should_panic/// use my_crate::UserId;/// UserId::parse("not-a-number").unwrap(); // panics/// ```pub fn parse(s: &str) -> Result<UserId, ParseError> { ... }
Fuzzing
// Cargo.toml in fuzz/ directory// [dependencies]// libfuzzer-sys = "0.4"// fuzz/fuzz_targets/parse_input.rs#![no_main]use libfuzzer_sys::fuzz_target;fuzz_target!(|data: &[u8]| {if let Ok(s) = std::str::from_utf8(data) {let _ = my_crate::parse(s); // Must not panic}});// Run: cargo fuzz run parse_input
Architecture Best Practices
Workspace layout
my-project/├── Cargo.toml # [workspace] members = [...]├── crates/│ ├── domain/ # Core types, traits, no I/O deps│ │ └── src/lib.rs│ ├── infrastructure/ # DB, HTTP clients, external APIs│ │ └── src/lib.rs│ ├── application/ # Business logic, orchestration│ │ └── src/lib.rs│ └── api/ # HTTP layer (axum/actix-web)│ └── src/│ ├── lib.rs│ └── main.rs└── tests/└── integration/
Rules:
domainmust not depend oninfrastructure— only pure types and traits.applicationdepends ondomaintraits, notinfrastructureconcretions.infrastructureimplementsdomaintraits; injected at startup inmain.- Compile-time enforcement of layering: wrong imports = compiler error.
Workspace discipline
# Root Cargo.toml — declare shared deps and policy ONCE[workspace.package]rust-version = "1.91" # MSRV floor, set by the most-constrained transitive dep[workspace.dependencies]tokio = { version = "1", features = ["full"] }serde = { version = "1", features = ["derive"] }
# Member Cargo.toml — reference, never re-pin[package]edition = "2021" # bump to "2024" ONLY if this crate needs async closures or other 2024-gated features[dependencies]tokio = { workspace = true }serde = { workspace = true }
Rules:
- Declare every shared external crate once in
[workspace.dependencies]; members referencedep = { workspace = true }. Never pin locally if it is already in the workspace table — local pins drift and force duplicate compiles. - MSRV (
rust-versionin[workspace.package]) is the floor imposed by the most-constrained TRANSITIVE dependency (e.g. a cloud SDK forcing a newer toolchain), not just the language features your code uses. Verify withcargo msrvor by auditing transitiverust-version. - Per-crate edition policy:
edition = "2024"only for crates that need async closures (async || {}) or other 2024-gated features; others stay2021. Note let-chains stabilized in Rust 1.88 and work in edition 2021 too, so they are not a reason to upgrade. Check the crate's ownCargo.toml, and verify withcargo checkbefore upgrading. - Cross-crate change protocol: edit lib →
cargo checkworkspace-wide →cargo test -p <lib>→cargo test -p <each consumer>→ commit all touchedCargo.tomltogether.
Feature flags
[features]default = ["tokio-runtime"]tokio-runtime = ["dep:tokio"]async-std-runtime = ["dep:async-std"]serde = ["dep:serde", "dep:serde_json"]tracing = ["dep:tracing"]# Optional dependencies (activated by feature)[dependencies]tokio = { version = "1", optional = true }serde = { version = "1", optional = true }
// Guard feature-specific code#[cfg(feature = "serde")]impl Serialize for MyType { ... }#[cfg(feature = "tracing")]tracing::info!("processing item");
# Library-and-binary crate: gate the HTTP stack so library consumers don't pull it in.[features]default = ["http-server"] # binary builds stay backward-compatiblehttp-server = ["dep:axum", "dep:tower-http"][dependencies]axum = { version = "0.7", optional = true }tower-http = { version = "0.6", optional = true }[[bin]]name = "serverd"required-features = ["http-server"]
Rules:
- If a crate is consumed as a library (not only built as a binary), mark
axum/tower-httpoptional = true, gate them behind anhttp-server(oraxum-server) feature, setdefault = ["http-server"]for backward-compatible binary builds, and addrequired-features = ["http-server"]on[[bin]]stanzas. Prevents forcing the full HTTP stack onto library consumers.
Module layout
// lib.rs: re-export public API cleanlypub use self::user::{User, UserId, UserError};pub use self::service::UserService;mod user; // user.rs or user/mod.rsmod service; // service.rs// Keep one public type per file for complex types// Internal modules use pub(crate) / pub(super) liberally
Shared state patterns
// AppState: clone-on-use (cheap because Arc internals)#[derive(Clone)]struct AppState {users: Arc<dyn UserRepository>,cache: Arc<dyn Cache>,config: Arc<Config>, // immutable after startup}// Pass via axum/actix State extractor, not global statics// Never use lazy_static or once_cell for mutable service state
Graceful shutdown
use tokio::signal;async fn run() -> anyhow::Result<()> {let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel(1);let server = tokio::spawn(serve(shutdown_rx));// Wait for Ctrl+C or SIGTERMsignal::ctrl_c().await?;let _ = shutdown_tx.send(());server.await??;Ok(())}async fn serve(mut shutdown: tokio::sync::broadcast::Receiver<()>) {loop {tokio::select! {_ = accept_connection() => { /* handle */ }_ = shutdown.recv() => break,}}}
Production graceful shutdown
// Prefer axum's built-in drain — finishes in-flight requests before exit.axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()).await?;async fn shutdown_signal() {let ctrl_c = async { signal::ctrl_c().await.expect("install SIGINT handler"); };#[cfg(unix)]let term = async {signal::unix::signal(signal::unix::SignalKind::terminate()).expect("install SIGTERM handler").recv().await;};#[cfg(not(unix))]let term = std::future::pending::<()>();tokio::select! { _ = ctrl_c => {}, _ = term => {} }}
Rules:
- Prefer
axum::serve(listener, app).with_graceful_shutdown(shutdown_signal())whereshutdown_signal()awaits BOTH SIGTERM and SIGINT (cfg-gated Unix/non-Unix). Draining in-flight requests lets storage fsync complete before exit. - Ops: systemd
KillSignal=SIGTERM+TimeoutStopSec=120(must exceed the fsync window on networked filesystems, e.g. EBS/EFS); on macOS uselaunchctl bootout(SIGTERM + drain), NOTlaunchctl kickstart -k(SIGKILL truncates in-flight writes). - Clients/bridges reconnect with exponential backoff (200ms → 30s cap) so daemon restarts are transparent.
Performance
- Avoid cloning: Restructure to use references or
Cowbefore reaching for.clone(). - `SmallVec<[T; N]>`: Use for vectors usually small; improves cache locality.
- Arena allocation:
bumpalofor batch-allocating many short-lived objects. - Capacity hints:
String::with_capacity(n),Vec::with_capacity(n)when final size is predictable. - Criterion benchmarks:
cargo benchgives statistical benchmarking with warmup. - Flamegraph profiling:
[profile.release] debug = truethencargo flamegraph. - LTO for binaries:
lto = "thin"+codegen-units = 1in release profile. - Measure first: Profile before optimizing any non-obvious path.
Why/What/Test Doc Pattern
/// Resolves the daemon's data directory, honouring an env override.////// Why: launchd/systemd strip $HOME, so dirs::* can return None at boot./// What: env override → dirs::data_local_dir() → passwd (getpwuid) fallback./// Test: tests/paths.rs::resolves_under_empty_env (covers the None branch).pub fn data_dir() -> anyhow::Result<PathBuf> { ... }
Rules:
- Non-trivial public
fn/struct/trait/moditems carry three doc-comment lines —Why:(the problem it solves),What:(mechanics),Test:(where coverage lives, or why untestable). "Non-trivial" in Rust is calibrated at roughly > 30 LOC or cyclomatic complexity > 5. - Trivial wrappers, one-liners, and simple getters (e.g.
pub fn is_empty(&self) -> bool { self.0.is_empty() }) need at most a single summary line. Forcing a three-line Why/What/Test triple on code shorter than its own docstring adds noise, not signal. - Clippy cannot enforce intent; this is how future readers reconstruct design without git archaeology.
Safety
- Minimize unsafe: Keep
unsafeblocks as small as possible; wrap in safe abstractions. - Safety comments: Every
unsafeblock requires// SAFETY:explaining invariants. - Rust 2024 `unsafe fn`: Function bodies are no longer implicitly unsafe.
- `unsafe extern` blocks: Rust 2024 requires
unsafe extern "C" { ... }. - `Send + Sync`: Auto-implemented when all fields qualify; verify with
static_assertions. - Interior mutability:
Cell<T>forCopy,RefCell<T>single-threaded,Mutex<T>multi-threaded.
Anti-Patterns to Avoid
- Excessive cloning: Do not clone to satisfy the borrow checker — restructure, use references, or
Cow. - Blocking in async: Never call
std::fs::read,std::net, orthread::sleepin async; usetokio::equivalents. - Unflushed async writes: A buffered
tokio::Filewrite withoutflush().awaitis a read-after-write data-loss race. - Timeout-free HTTP client:
reqwest::Client::new()in a service hangs forever on a slow upstream — alwaysClientBuilderwith timeouts. - Bare `unwrap()` in production: Replace with
?,.unwrap_or_default(),.expect("invariant: reason"). - Stringly-typed errors: No
StringorBox<dyn Error>in library APIs — define typed enums withthiserror. - Half-initialized objects: Never expose constructors that leave fields unset — use builder pattern.
- `Deref` for inheritance: Do not implement
Derefto simulate OOP — use trait composition. - `env::set_var` without `unsafe`: In Rust 2024,
set_var/remove_varareunsafe. - Clippy silencing:
cargo clippy -- -D warningsin CI; use#[allow]with justification, never blanket silencing. - Global state for DI: Do not use
lazy_static/once_cellfor mutable services — constructor inject.
Rust Daemon & Supervised-Process Patterns
A daemon launched by launchd/systemd runs in a stripped environment with aggressive respawn policies. The patterns below assume that supervisor and cross-reference the production graceful-shutdown guidance under Architecture Best Practices.
Path resolution under supervision
/// Resolve the data directory: env override → dirs → passwd fallback.////// Why: under posix_spawn (launchd/systemd) NSFileManager/HOME may be/// uninitialised, so dirs::data_local_dir() can return None./// What: validated env override wins; otherwise dirs, then getpwuid($HOME)./// Test: tests/paths.rs::env_empty_falls_back_to_passwd.pub fn data_dir() -> anyhow::Result<PathBuf> {if let Ok(raw) = std::env::var("APP_DATA_DIR") {let trimmed = raw.trim();anyhow::ensure!(!trimmed.is_empty(), "APP_DATA_DIR is empty");let p = PathBuf::from(trimmed);anyhow::ensure!(p.is_absolute(), "APP_DATA_DIR must be absolute: {p:?}");anyhow::ensure!(p != Path::new("/"), "APP_DATA_DIR must not be root");return Ok(p);}let dir = dirs::data_local_dir() // may be None under a supervisor.or_else(passwd_home_data_dir) // getpwuid() fallback — mandatory.ok_or_else(|| anyhow::anyhow!("cannot resolve data dir"))?;tracing::info!(path = %dir.display(), "resolved data dir"); // log every bootOk(dir)}
Rules:
- Resolve the data dir as env override →
dirs::data_local_dir()→$HOME/passwd (getpwuid) fallback. The passwd fallback is mandatory: underposix_spawn,dirs::*can returnNonebecause HOME/NSFileManager is not initialised. - Validate any env override is absolute (
anyhow::ensure!(p.is_absolute())); reject empty/whitespace/relative/root. - Log the resolved path at INFO on every boot — first thing you check when a supervised daemon "loses" its data.
File-descriptor limits
# systemd unit[Service]LimitNOFILE=8192
<!-- launchd plist --><key>SoftResourceLimits</key> <dict><key>NumberOfFiles</key><integer>8192</integer></dict><key>HardResourceLimits</key> <dict><key>NumberOfFiles</key><integer>8192</integer></dict>
Rules:
- Set
LimitNOFILE=8192(systemd) /SoftResourceLimits+HardResourceLimitsNumberOfFiles: 8192(launchd). The macOS launchd default is 256 — a daemon holding many DB files (e.g. N stores × 3 files) hits EMFILE. - Combined with
KeepAlive/Restart=always, an EMFILE crash becomes a respawn storm. Exposeopen_fds+fd_soft_limitin the/healthresponse for early warning.
Single-instance guard
// On startup, probe the recorded health address before binding.if let Ok(resp) = http_get(&recorded_health_url).await {if resp.is_healthy() {tracing::info!("healthy incumbent already running; exiting");std::process::exit(0); // NOT an error — stops supervisor respawn storm}}bind_and_serve(addr).await
Rules:
- On startup probe the recorded health address; if a healthy incumbent answers,
std::process::exit(0)instead of binding. This stops launchd/systemd respawn storms without external intervention (a second instance exiting cleanly is success, not failure). std::process::exitskips destructors, so flush any buffered logs/tracing (e.g. a non-blockingtracing-appenderworker guard) before calling it — or place the guard before subscriber/buffer init — otherwise the "exiting" line may never be written.
MCP stdio framing
// stdio JSON-RPC server: stdout is the protocol channel — keep it pristine.loop {let line = match read_line(&mut stdin).await? {Some(l) => l,None => std::process::exit(0), // EOF — exit so idle reqwest pool tasks don't pin the runtime};let resp = handle(&line).await;write_framed(&mut stdout, &resp).await?; // ONLY protocol frames go to stdout}
Rules:
- In a stdio JSON-RPC server, ALL logs go to stderr — a stray
println!corrupts the protocol framing. Routetracingto stderr explicitly. - The stdio serve loop must call
std::process::exit(0)on stdin EOF; otherwisereqwestidle-pool background tasks keep the tokio runtime alive and orphaned workers accumulate across client restarts.
Rust Embedded Storage Discipline
Patterns for embedded key/value and document stores (redb / sled / SQLite) with bincode/serde payloads. The recurring failure mode is a binary upgrade that comes up "healthy but empty" — these rules make degradation impossible without an explicit, recoverable decision.
Graceful format-upgrade handling
// Classify EVERY open/create error before deciding what to do.fn open_or_recreate(path: &Path) -> anyhow::Result<Db> {match Db::open(path) {Ok(db) => Ok(db),Err(e) if is_format_error(&e) => { // UpgradeRequired / RepairAborted / Corruptedrun_migration_or_warn(path)?; // prefer data-preserving migrationrecreate(path) // destructive ONLY for format breaks}Err(e) if is_lock_error(&e) => Err(e.into()), // DatabaseAlreadyOpen — NEVER recreateErr(e) => Err(e.into()), // transient I/O — NEVER recreate}}
Rules:
- At EVERY open/create site classify errors:
UpgradeRequired/RepairAborted/Corrupted= incompatible on-disk format;DatabaseAlreadyOpen= lock contention; everything else = transient I/O. - Destructive recreate ONLY on format errors — NEVER on lock or I/O errors (a lock error means another instance owns live data; an I/O error is transient).
- Ship a data-preserving migration tool for format breaks. A binary upgrade must never silently degrade to "healthy but empty".
Atomic swap for durable rebuilds
// Reindex/compaction/migration writes to staging; swap only on validated success.let staging = root.join("index.staging");build_into(&staging)?;anyhow::ensure!(record_count(&staging)? > 0, "rebuild produced 0 records — Failed, not Ready");fs::rename(&staging, root.join("index.live"))?; // atomic swap// On any failure/empty/abort: discard staging, leave previous live data intact.
Rules:
- Any reindex/compaction/migration writes to a STAGING location and swaps atomically only on success; on failure/empty/abort, discard the staged copy and leave the previous live data intact.
- Validate non-empty before marking ready — a pipeline that walked files but produced zero records is
Failed, notReady(the false-green antipattern).
Root-relative path storage
// Store paths relative to a canonical root — survives moves, remounts, cross-platform copies.let root = root.canonicalize()?; // so strip_prefix reliably yields relativelet rel = abs_path.strip_prefix(&root)?.to_path_buf();store.put(key, &rel)?; // persist relative, never absolute
Rules:
- Store file paths relative to a known root, never absolute — survives data-dir moves, remounts, and cross-platform copies. Canonicalize the root so
strip_prefixreliably yields a relative path. - Provide an idempotent, schema-versioned migration that detects and rewrites legacy absolute paths on warm boot.