feat(p2p): add production infrastructure modules
- error.rs: Structured error types with context for all subsystems (transport, routing, crypto, protocol, store, config) - config.rs: Runtime configuration with TOML parsing and validation - metrics.rs: Counter/gauge/histogram metrics with transport-specific tracking and JSON-serializable snapshots - rate_limit.rs: Token bucket rate limiting with per-peer tracking, duty cycle enforcement for LoRa, and backpressure control These modules provide the foundation for production deployment.
This commit is contained in:
354
crates/quicprochat-p2p/src/error.rs
Normal file
354
crates/quicprochat-p2p/src/error.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
//! Production-ready error types for the mesh P2P layer.
|
||||
//!
|
||||
//! This module provides structured error types with context for debugging
|
||||
//! and recovery. Errors are categorized by subsystem for easier handling.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::address::MeshAddress;
|
||||
use crate::transport::TransportAddr;
|
||||
|
||||
/// Top-level mesh error type.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MeshError {
|
||||
/// Transport layer errors.
|
||||
#[error("transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
|
||||
/// Routing errors.
|
||||
#[error("routing error: {0}")]
|
||||
Routing(#[from] RoutingError),
|
||||
|
||||
/// Crypto/encryption errors.
|
||||
#[error("crypto error: {0}")]
|
||||
Crypto(#[from] CryptoError),
|
||||
|
||||
/// Protocol errors (malformed messages, version mismatch).
|
||||
#[error("protocol error: {0}")]
|
||||
Protocol(#[from] ProtocolError),
|
||||
|
||||
/// Store/cache errors.
|
||||
#[error("store error: {0}")]
|
||||
Store(#[from] StoreError),
|
||||
|
||||
/// Configuration errors.
|
||||
#[error("config error: {0}")]
|
||||
Config(#[from] ConfigError),
|
||||
|
||||
/// Internal errors (bugs, invariant violations).
|
||||
#[error("internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
/// Transport layer errors.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TransportError {
|
||||
/// Failed to send data.
|
||||
#[error("send failed to {dest}: {reason}")]
|
||||
SendFailed { dest: String, reason: String },
|
||||
|
||||
/// Failed to receive data.
|
||||
#[error("receive failed: {0}")]
|
||||
ReceiveFailed(String),
|
||||
|
||||
/// Connection failed or lost.
|
||||
#[error("connection to {dest} failed: {reason}")]
|
||||
ConnectionFailed { dest: String, reason: String },
|
||||
|
||||
/// Transport not available.
|
||||
#[error("transport '{name}' not available")]
|
||||
NotAvailable { name: String },
|
||||
|
||||
/// No transports registered.
|
||||
#[error("no transports registered")]
|
||||
NoTransports,
|
||||
|
||||
/// MTU exceeded.
|
||||
#[error("payload {size} bytes exceeds MTU {mtu} bytes")]
|
||||
MtuExceeded { size: usize, mtu: usize },
|
||||
|
||||
/// Duty cycle limit reached.
|
||||
#[error("duty cycle limit reached: {used_ms}ms used of {limit_ms}ms allowed")]
|
||||
DutyCycleExceeded { used_ms: u64, limit_ms: u64 },
|
||||
|
||||
/// Timeout waiting for response.
|
||||
#[error("timeout waiting for response from {dest}")]
|
||||
Timeout { dest: String },
|
||||
|
||||
/// I/O error.
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Routing errors.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RoutingError {
|
||||
/// No route to destination.
|
||||
#[error("no route to {0}")]
|
||||
NoRoute(String),
|
||||
|
||||
/// Route expired.
|
||||
#[error("route to {dest} expired (last seen {age_secs}s ago)")]
|
||||
RouteExpired { dest: String, age_secs: u64 },
|
||||
|
||||
/// Too many hops.
|
||||
#[error("max hops ({max}) exceeded for message to {dest}")]
|
||||
MaxHopsExceeded { dest: String, max: u8 },
|
||||
|
||||
/// Message expired.
|
||||
#[error("message expired (TTL {ttl_secs}s, age {age_secs}s)")]
|
||||
MessageExpired { ttl_secs: u32, age_secs: u64 },
|
||||
|
||||
/// Duplicate message (dedup).
|
||||
#[error("duplicate message ID {0}")]
|
||||
Duplicate(String),
|
||||
|
||||
/// Routing table full.
|
||||
#[error("routing table full ({capacity} entries)")]
|
||||
TableFull { capacity: usize },
|
||||
}
|
||||
|
||||
/// Crypto/encryption errors.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CryptoError {
|
||||
/// Signature verification failed.
|
||||
#[error("signature verification failed for {context}")]
|
||||
SignatureInvalid { context: String },
|
||||
|
||||
/// Decryption failed.
|
||||
#[error("decryption failed: {0}")]
|
||||
DecryptionFailed(String),
|
||||
|
||||
/// Key not found.
|
||||
#[error("key not found for {0}")]
|
||||
KeyNotFound(String),
|
||||
|
||||
/// KeyPackage invalid or expired.
|
||||
#[error("KeyPackage invalid: {0}")]
|
||||
KeyPackageInvalid(String),
|
||||
|
||||
/// Replay attack detected.
|
||||
#[error("replay detected: sequence {seq} already seen from {sender}")]
|
||||
ReplayDetected { sender: String, seq: u32 },
|
||||
|
||||
/// Wrong epoch.
|
||||
#[error("wrong epoch: expected {expected}, got {got}")]
|
||||
WrongEpoch { expected: u16, got: u16 },
|
||||
|
||||
/// MLS error (from openmls).
|
||||
#[error("MLS error: {0}")]
|
||||
Mls(String),
|
||||
}
|
||||
|
||||
/// Protocol errors.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProtocolError {
|
||||
/// Unknown message type.
|
||||
#[error("unknown message type: 0x{0:02x}")]
|
||||
UnknownMessageType(u8),
|
||||
|
||||
/// Invalid message format.
|
||||
#[error("invalid message format: {0}")]
|
||||
InvalidFormat(String),
|
||||
|
||||
/// Version mismatch.
|
||||
#[error("protocol version mismatch: expected {expected}, got {got}")]
|
||||
VersionMismatch { expected: u8, got: u8 },
|
||||
|
||||
/// Required field missing.
|
||||
#[error("required field missing: {0}")]
|
||||
MissingField(String),
|
||||
|
||||
/// CBOR decode error.
|
||||
#[error("CBOR decode error: {0}")]
|
||||
CborDecode(String),
|
||||
|
||||
/// CBOR encode error.
|
||||
#[error("CBOR encode error: {0}")]
|
||||
CborEncode(String),
|
||||
|
||||
/// Message too large.
|
||||
#[error("message too large: {size} bytes (max {max})")]
|
||||
MessageTooLarge { size: usize, max: usize },
|
||||
}
|
||||
|
||||
/// Store/cache errors.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum StoreError {
|
||||
/// Store is full.
|
||||
#[error("store full: {current}/{capacity} items")]
|
||||
Full { current: usize, capacity: usize },
|
||||
|
||||
/// Item not found.
|
||||
#[error("item not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
/// Persistence error.
|
||||
#[error("persistence error: {0}")]
|
||||
Persistence(String),
|
||||
|
||||
/// Serialization error.
|
||||
#[error("serialization error: {0}")]
|
||||
Serialization(String),
|
||||
}
|
||||
|
||||
/// Configuration errors.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
/// Invalid configuration value.
|
||||
#[error("invalid config value for '{key}': {reason}")]
|
||||
InvalidValue { key: String, reason: String },
|
||||
|
||||
/// Missing required configuration.
|
||||
#[error("missing required config: {0}")]
|
||||
Missing(String),
|
||||
|
||||
/// Configuration parse error.
|
||||
#[error("config parse error: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
/// Result type alias for mesh operations.
|
||||
pub type MeshResult<T> = Result<T, MeshError>;
|
||||
|
||||
/// Error context extension trait for adding context to errors.
|
||||
pub trait ErrorContext<T> {
|
||||
/// Add context to an error.
|
||||
fn context(self, context: impl Into<String>) -> MeshResult<T>;
|
||||
|
||||
/// Add context with a closure (lazy evaluation).
|
||||
fn with_context<F>(self, f: F) -> MeshResult<T>
|
||||
where
|
||||
F: FnOnce() -> String;
|
||||
}
|
||||
|
||||
impl<T, E: Into<MeshError>> ErrorContext<T> for Result<T, E> {
|
||||
fn context(self, context: impl Into<String>) -> MeshResult<T> {
|
||||
self.map_err(|e| {
|
||||
let err = e.into();
|
||||
MeshError::Internal(format!("{}: {}", context.into(), err))
|
||||
})
|
||||
}
|
||||
|
||||
fn with_context<F>(self, f: F) -> MeshResult<T>
|
||||
where
|
||||
F: FnOnce() -> String,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let err = e.into();
|
||||
MeshError::Internal(format!("{}: {}", f(), err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert anyhow errors to MeshError.
|
||||
impl From<anyhow::Error> for MeshError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
MeshError::Internal(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create transport send errors.
|
||||
impl TransportError {
|
||||
pub fn send_failed(dest: &TransportAddr, reason: impl Into<String>) -> Self {
|
||||
Self::SendFailed {
|
||||
dest: dest.to_string(),
|
||||
reason: reason.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connection_failed(dest: &TransportAddr, reason: impl Into<String>) -> Self {
|
||||
Self::ConnectionFailed {
|
||||
dest: dest.to_string(),
|
||||
reason: reason.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create routing errors.
|
||||
impl RoutingError {
|
||||
pub fn no_route(addr: &MeshAddress) -> Self {
|
||||
Self::NoRoute(format!("{}", addr))
|
||||
}
|
||||
|
||||
pub fn no_route_bytes(addr: &[u8]) -> Self {
|
||||
Self::NoRoute(hex::encode(&addr[..8.min(addr.len())]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create crypto errors.
|
||||
impl CryptoError {
|
||||
pub fn signature_invalid(context: impl Into<String>) -> Self {
|
||||
Self::SignatureInvalid {
|
||||
context: context.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replay(sender: &MeshAddress, seq: u32) -> Self {
|
||||
Self::ReplayDetected {
|
||||
sender: format!("{}", sender),
|
||||
seq,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create protocol errors.
|
||||
impl ProtocolError {
|
||||
pub fn cbor_decode(e: impl fmt::Display) -> Self {
|
||||
Self::CborDecode(e.to_string())
|
||||
}
|
||||
|
||||
pub fn cbor_encode(e: impl fmt::Display) -> Self {
|
||||
Self::CborEncode(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn error_display() {
|
||||
let err = TransportError::SendFailed {
|
||||
dest: "tcp:127.0.0.1:8080".to_string(),
|
||||
reason: "connection refused".to_string(),
|
||||
};
|
||||
assert!(err.to_string().contains("tcp:127.0.0.1:8080"));
|
||||
assert!(err.to_string().contains("connection refused"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_conversion() {
|
||||
let transport_err = TransportError::NoTransports;
|
||||
let mesh_err: MeshError = transport_err.into();
|
||||
assert!(matches!(mesh_err, MeshError::Transport(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routing_error_helpers() {
|
||||
let addr = MeshAddress::from_bytes([0xAB; 16]);
|
||||
let err = RoutingError::no_route(&addr);
|
||||
assert!(err.to_string().contains("no route"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crypto_error_helpers() {
|
||||
let addr = MeshAddress::from_bytes([0xCD; 16]);
|
||||
let err = CryptoError::replay(&addr, 42);
|
||||
assert!(err.to_string().contains("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_extension() {
|
||||
fn fallible() -> Result<(), TransportError> {
|
||||
Err(TransportError::NoTransports)
|
||||
}
|
||||
|
||||
let result: MeshResult<()> = fallible().context("during startup");
|
||||
assert!(result.is_err());
|
||||
let err_str = result.unwrap_err().to_string();
|
||||
assert!(err_str.contains("during startup"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user