一键导入
toolchains-rust-core
Rust 2024 edition core patterns: idiomatic code, error handling, traits/generics, macros, async/concurrency, testing, and project architecture
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Rust 2024 edition core patterns: idiomatic code, error handling, traits/generics, macros, async/concurrency, testing, and project architecture
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
MCP (Model Context Protocol) - Build AI-native servers with tools, resources, and prompts. TypeScript/Python SDKs for Claude Desktop integration.
Model Context Protocol (MCP) server build and evaluation guide, including local conventions for tool surfaces, config, and testing
Control monitoring server and dashboard
Pause session and save current work state for later resume
Load context from paused session
Access Claude MPM functionality and manage multi-agent orchestration
| name | 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 |
# 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/derive
cargo clippy -- -D warnings # enforce in CI
cargo test # unit + integration + doctests
cargo test --doc # doctests only
self methods; finish with build() -> Result<T, E> for validated construction.struct UserId(u64) gives type safety at zero cost; add impl From<u64> for UserId.From<T> only — Into<T> is blanket-provided; accept impl Into<T> in function params.#[derive(Debug, Clone, PartialEq, Eq, Hash)] on domain types; add serde only when needed.&str not &String, &[T] not &Vec<T>, impl AsRef<Path> not &PathBuf.Config { field: value, ..Default::default() } — audit all fields when adding new ones.Drop for resource cleanup; wrap locks, file handles, and connections in guard types.Cow<'_, str>: Return Cow<'_, str> when sometimes borrowing, sometimes owning — avoids unnecessary clones.Box only for trait objects, recursive types, or large data escaping scope.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:
String or Box<dyn Error> in public library API.#[from] generates From<SourceError> — use ? to propagate automatically.#[source] exposes the underlying error via Error::source() without auto-From.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> in main(), CLI handlers, and top-level application code..context("what we were doing") at each call site.anyhow in public library APIs — wrap and convert at the boundary.// In application code: convert library error to anyhow
let user = user_service.get(id)
.await
.map_err(|e| anyhow::anyhow!("get user {id}: {e}"))?;
// Or implement From in your app error type
impl From<UserError> for AppError { ... }
// Prefer small traits over large catch-all interfaces
trait Fetch {
async fn fetch(&self, url: &str) -> Result<Bytes, FetchError>;
}
trait Store {
async fn store(&self, key: &str, value: &[u8]) -> Result<(), StoreError>;
}
// Compose with supertraits
trait Cache: Fetch + Store + Send + Sync {}
// Generics: compile-time dispatch, monomorphized, no heap alloc
fn process<F: Fetch>(fetcher: &F) -> Result<()> { ... }
// Trait objects: runtime dispatch, heap alloc, heterogeneous collections
fn 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 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 type
trait Convert<T> {
fn convert(&self) -> T;
}
// allows: impl Convert<String> for MyType AND impl Convert<u64> for MyType
// Inline bounds for simple cases
fn 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>
where
T: Display + Send + 'static,
E: std::error::Error + From<fmt::Error>,
{ ... }
// Provide default impl for all qualifying types
trait Summarize {
fn summary(&self) -> String;
}
impl<T: Display> Summarize for T {
fn summary(&self) -> String {
format!("{}", self)
}
}
trait IteratorExt: Iterator {
fn take_while_inclusive<P>(self, predicate: P) -> TakeWhileInclusive<Self, P>
where
P: 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 }
}
}
mod private {
pub trait Sealed {}
}
pub trait MyTrait: private::Sealed {
fn method(&self);
}
// Only types you impl private::Sealed for can impl MyTrait
impl private::Sealed for MyType {}
impl MyTrait for MyType { fn method(&self) {} }
// Stable since Rust 1.65 — enables lifetime-parameterized associated types
trait Lender {
type Item<'a> where Self: 'a;
fn lend(&mut self) -> Self::Item<'_>;
}
macro_rules!)// Pattern matching on token trees
macro_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 patterns
macro_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)
}
})+
};
}
// 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:
$crate:: prefix for items referenced inside the macro.derive_more and strum crates over writing your own derive macros.trybuild to verify compile-time error messages.#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Tokio only — async-std is effectively unmaintained as of 2026
run().await
}
// Async closures (Rust 2024)
let handler = async |req: Request| -> Result<Response> {
process(req).await
};
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 groups
use 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??);
}
// Arc<Mutex<T>> for mutable shared state
let 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::Mutex
guard.insert(1, user);
});
// RwLock for read-heavy workloads
let 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 overhead
use std::sync::atomic::{AtomicU64, Ordering};
let counter = Arc::new(AtomicU64::new(0));
counter.fetch_add(1, Ordering::SeqCst);
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;
}
}
// RAII cleanup: use Drop for cancellation handling
struct Guard {
resource: Resource,
}
impl Drop for Guard {
fn drop(&mut self) {
// Always runs, even if task is cancelled
self.resource.cleanup();
}
}
// Avoid holding mutable state across .await points
// BAD:
let mut locked = mutex.lock().await;
do_io().await; // lock held across await — deadlock risk
drop(locked);
// GOOD:
{
let mut locked = mutex.lock().await;
update_state(&mut locked);
} // lock released before await
do_io().await;
// Never block the async executor
let result = tokio::task::spawn_blocking(|| {
// CPU-intensive or blocking sync code here
heavy_computation()
})
.await?;
// Rayon for data-parallel CPU work
use rayon::prelude::*;
let result: Vec<_> = data.par_iter().map(|x| transform(x)).collect();
// 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:
tokio::fs::File::write_all(), call flush().await before any reader runs or you return — otherwise silent read-after-write race / data loss.tokio::fs::File defers writes to a blocking thread pool — flush().await ensures all queued writes have completed before a reader runs (it is NOT a userspace BufWriter). The free fn tokio::fs::write() is a single open/write/close. Sync std::fs::File has no Rust-layer userspace buffer, so writes reach the OS page cache on return — but crash/power-loss durability still requires sync_all()/sync_data(); drop alone does not fsync.// 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:
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).timeout and a connect_timeout; build once and clone (the inner pool is shared).// Unit tests: same file, cfg(test) module
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_operation() {
// Arrange
let sut = MyStruct::new(42);
// Act
let result = sut.compute();
// Assert
assert_eq!(result, 84);
}
}
// Integration tests: tests/ directory, no cfg(test) needed
// tests/integration_test.rs
use my_crate::Api;
#[tokio::test]
async fn full_roundtrip() {
let api = Api::start_test_server().await;
// ...
}
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());
}
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);
}
}
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"
}
"###);
}
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();
}
}
// 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() { ... }
/// 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> { ... }
// 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
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:
domain must not depend on infrastructure — only pure types and traits.application depends on domain traits, not infrastructure concretions.infrastructure implements domain traits; injected at startup in main.# 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:
[workspace.dependencies]; members reference dep = { workspace = true }. Never pin locally if it is already in the workspace table — local pins drift and force duplicate compiles.rust-version in [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 with cargo msrv or by auditing transitive rust-version.edition = "2024" only for crates that need async closures (async || {}) or other 2024-gated features; others stay 2021. 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 own Cargo.toml, and verify with cargo check before upgrading.cargo check workspace-wide → cargo test -p <lib> → cargo test -p <each consumer> → commit all touched Cargo.toml together.[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-compatible
http-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:
axum/tower-http optional = true, gate them behind an http-server (or axum-server) feature, set default = ["http-server"] for backward-compatible binary builds, and add required-features = ["http-server"] on [[bin]] stanzas. Prevents forcing the full HTTP stack onto library consumers.// lib.rs: re-export public API cleanly
pub use self::user::{User, UserId, UserError};
pub use self::service::UserService;
mod user; // user.rs or user/mod.rs
mod service; // service.rs
// Keep one public type per file for complex types
// Internal modules use pub(crate) / pub(super) liberally
// 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
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 SIGTERM
signal::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,
}
}
}
// 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:
axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()) where shutdown_signal() awaits BOTH SIGTERM and SIGINT (cfg-gated Unix/non-Unix). Draining in-flight requests lets storage fsync complete before exit.KillSignal=SIGTERM + TimeoutStopSec=120 (must exceed the fsync window on networked filesystems, e.g. EBS/EFS); on macOS use launchctl bootout (SIGTERM + drain), NOT launchctl kickstart -k (SIGKILL truncates in-flight writes).Cow before reaching for .clone().SmallVec<[T; N]>: Use for vectors usually small; improves cache locality.bumpalo for batch-allocating many short-lived objects.String::with_capacity(n), Vec::with_capacity(n) when final size is predictable.cargo bench gives statistical benchmarking with warmup.[profile.release] debug = true then cargo flamegraph.lto = "thin" + codegen-units = 1 in release profile./// 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:
fn/struct/trait/mod items 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.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.unsafe blocks as small as possible; wrap in safe abstractions.unsafe block requires // SAFETY: explaining invariants.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.Cell<T> for Copy, RefCell<T> single-threaded, Mutex<T> multi-threaded.Cow.std::fs::read, std::net, or thread::sleep in async; use tokio:: equivalents.tokio::File write without flush().await is a read-after-write data-loss race.reqwest::Client::new() in a service hangs forever on a slow upstream — always ClientBuilder with timeouts.unwrap() in production: Replace with ?, .unwrap_or_default(), .expect("invariant: reason").String or Box<dyn Error> in library APIs — define typed enums with thiserror.Deref for inheritance: Do not implement Deref to simulate OOP — use trait composition.env::set_var without unsafe: In Rust 2024, set_var/remove_var are unsafe.cargo clippy -- -D warnings in CI; use #[allow] with justification, never blanket silencing.lazy_static/once_cell for mutable services — constructor inject.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.
/// 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 boot
Ok(dir)
}
Rules:
dirs::data_local_dir() → $HOME/passwd (getpwuid) fallback. The passwd fallback is mandatory: under posix_spawn, dirs::* can return None because HOME/NSFileManager is not initialised.anyhow::ensure!(p.is_absolute())); reject empty/whitespace/relative/root.# 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:
LimitNOFILE=8192 (systemd) / SoftResourceLimits+HardResourceLimits NumberOfFiles: 8192 (launchd). The macOS launchd default is 256 — a daemon holding many DB files (e.g. N stores × 3 files) hits EMFILE.KeepAlive/Restart=always, an EMFILE crash becomes a respawn storm. Expose open_fds + fd_soft_limit in the /health response for early warning.// 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:
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::exit skips destructors, so flush any buffered logs/tracing (e.g. a non-blocking tracing-appender worker guard) before calling it — or place the guard before subscriber/buffer init — otherwise the "exiting" line may never be written.// 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:
println! corrupts the protocol framing. Route tracing to stderr explicitly.std::process::exit(0) on stdin EOF; otherwise reqwest idle-pool background tasks keep the tokio runtime alive and orphaned workers accumulate across client restarts.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.
// 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 / Corrupted
run_migration_or_warn(path)?; // prefer data-preserving migration
recreate(path) // destructive ONLY for format breaks
}
Err(e) if is_lock_error(&e) => Err(e.into()), // DatabaseAlreadyOpen — NEVER recreate
Err(e) => Err(e.into()), // transient I/O — NEVER recreate
}
}
Rules:
UpgradeRequired/RepairAborted/Corrupted = incompatible on-disk format; DatabaseAlreadyOpen = lock contention; everything else = transient I/O.// 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:
Failed, not Ready (the false-green antipattern).// Store paths relative to a canonical root — survives moves, remounts, cross-platform copies.
let root = root.canonicalize()?; // so strip_prefix reliably yields relative
let rel = abs_path.strip_prefix(&root)?.to_path_buf();
store.put(key, &rel)?; // persist relative, never absolute
Rules:
strip_prefix reliably yields a relative path.