use crate::types::{RequestId, TenantId, VShardId};
use nodedb_types::error::NodeDbError;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("constraint violation on {collection}: {detail}")]
RejectedConstraint {
collection: String,
constraint: String,
detail: String,
},
#[error("authorization denied for tenant {tenant_id} on {resource}")]
RejectedAuthz {
tenant_id: TenantId,
resource: String,
},
#[error("request {request_id} exceeded deadline")]
DeadlineExceeded { request_id: RequestId },
#[error("write conflict on {collection}/{document_id}, retry with idempotency key")]
ConflictRetry {
collection: String,
document_id: String,
},
#[error("CRDT delta pre-validation rejected: {constraint} — {reason}")]
RejectedPrevalidation { constraint: String, reason: String },
#[error("collection {collection} not found for tenant {tenant_id}")]
CollectionNotFound {
tenant_id: TenantId,
collection: String,
},
#[error("document {document_id} not found in {collection}")]
DocumentNotFound {
collection: String,
document_id: String,
},
#[error("vshard {vshard_id} has no serving leader")]
NoLeader { vshard_id: VShardId },
#[error("not leader for vshard {vshard_id}; leader is node {leader_node} at {leader_addr}")]
NotLeader {
vshard_id: VShardId,
leader_node: u64,
leader_addr: String,
},
#[error("query fan-out exceeded: {shards_touched} shards > limit {limit}")]
FanOutExceeded { shards_touched: u16, limit: u16 },
#[error("bad request: {detail}")]
BadRequest { detail: String },
#[error("query plan error: {detail}")]
PlanError { detail: String },
#[error("WAL error: {0}")]
Wal(#[from] nodedb_wal::WalError),
#[error("dispatch error: {detail}")]
Dispatch { detail: String },
#[error("storage error ({engine}): {detail}")]
Storage { engine: String, detail: String },
#[error("cold storage error: {detail}")]
ColdStorage { detail: String },
#[error("serialization error ({format}): {detail}")]
Serialization { format: String, detail: String },
#[error("codec error: {detail}")]
Codec { detail: String },
#[error("segment corrupted: {detail}")]
SegmentCorrupted { detail: String },
#[error("memory budget exhausted for engine {engine}")]
MemoryExhausted { engine: String },
#[error("CRDT engine error: {0}")]
Crdt(#[from] nodedb_crdt::CrdtError),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("configuration error: {detail}")]
Config { detail: String },
#[error("encryption error: {detail}")]
Encryption { detail: String },
#[error("bridge error: {detail}")]
Bridge { detail: String },
#[error("version compatibility: {detail}")]
VersionCompat { detail: String },
#[error("internal error: {detail}")]
Internal { detail: String },
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<crate::control::pubsub::TopicError> for Error {
fn from(e: crate::control::pubsub::TopicError) -> Self {
Self::BadRequest {
detail: e.to_string(),
}
}
}
impl From<crate::engine::timeseries::ilp::IlpError> for Error {
fn from(e: crate::engine::timeseries::ilp::IlpError) -> Self {
Self::BadRequest {
detail: e.to_string(),
}
}
}
impl From<crate::engine::timeseries::columnar_segment::SegmentError> for Error {
fn from(e: crate::engine::timeseries::columnar_segment::SegmentError) -> Self {
Self::Storage {
engine: "timeseries".into(),
detail: e.to_string(),
}
}
}
impl From<crate::engine::timeseries::query::QueryError> for Error {
fn from(e: crate::engine::timeseries::query::QueryError) -> Self {
Self::Storage {
engine: "timeseries".into(),
detail: e.to_string(),
}
}
}
impl From<crate::control::security::crl::CrlError> for Error {
fn from(e: crate::control::security::crl::CrlError) -> Self {
Self::Config {
detail: e.to_string(),
}
}
}
impl From<crate::control::security::jwt::JwtError> for Error {
fn from(e: crate::control::security::jwt::JwtError) -> Self {
Self::RejectedAuthz {
tenant_id: TenantId::new(0),
resource: e.to_string(),
}
}
}
impl From<Error> for NodeDbError {
fn from(e: Error) -> Self {
match e {
Error::RejectedConstraint {
collection, detail, ..
} => NodeDbError::constraint_violation(collection, detail),
Error::RejectedAuthz { resource, .. } => NodeDbError::authorization_denied(resource),
Error::DeadlineExceeded { .. } => NodeDbError::deadline_exceeded(),
Error::ConflictRetry {
collection,
document_id,
} => NodeDbError::write_conflict(collection, document_id),
Error::RejectedPrevalidation { constraint, reason } => {
NodeDbError::prevalidation_rejected(constraint, reason)
}
Error::CollectionNotFound { collection, .. } => {
NodeDbError::collection_not_found(collection)
}
Error::DocumentNotFound {
collection,
document_id,
} => NodeDbError::document_not_found(collection, document_id),
Error::NoLeader { vshard_id } => {
NodeDbError::no_leader(format!("vshard {vshard_id} has no serving leader"))
}
Error::NotLeader { leader_addr, .. } => NodeDbError::not_leader(leader_addr),
Error::FanOutExceeded {
shards_touched,
limit,
} => NodeDbError::fan_out_exceeded(shards_touched, limit),
Error::BadRequest { detail } => NodeDbError::bad_request(detail),
Error::PlanError { detail } => NodeDbError::plan_error(detail),
Error::Wal(wal_err) => NodeDbError::wal(wal_err),
Error::Dispatch { detail } => NodeDbError::dispatch(detail),
Error::Storage { detail, .. } => NodeDbError::storage(detail),
Error::ColdStorage { detail } => NodeDbError::cold_storage(detail),
Error::Serialization { format, detail } => NodeDbError::serialization(format, detail),
Error::Codec { detail } => NodeDbError::codec(detail),
Error::SegmentCorrupted { detail } => NodeDbError::segment_corrupted(detail),
Error::MemoryExhausted { engine } => NodeDbError::memory_exhausted(engine),
Error::Crdt(crdt_err) => NodeDbError::internal(crdt_err),
Error::Io(io_err) => NodeDbError::storage(io_err),
Error::Config { detail } => NodeDbError::config(detail),
Error::Encryption { detail } => NodeDbError::encryption(detail),
Error::Bridge { detail } => NodeDbError::bridge(detail),
Error::VersionCompat { detail } => NodeDbError::cluster(detail),
Error::Internal { detail } => NodeDbError::internal(detail),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display_constraint() {
let e = Error::RejectedConstraint {
collection: "users".into(),
constraint: "users_email_unique".into(),
detail: "duplicate email".into(),
};
assert!(e.to_string().contains("constraint violation"));
assert!(e.to_string().contains("users"));
}
#[test]
fn error_display_deadline() {
let e = Error::DeadlineExceeded {
request_id: RequestId::new(42),
};
assert!(e.to_string().contains("req:42"));
assert!(e.to_string().contains("deadline"));
}
#[test]
fn error_display_fan_out() {
let e = Error::FanOutExceeded {
shards_touched: 32,
limit: 16,
};
assert!(e.to_string().contains("32"));
assert!(e.to_string().contains("16"));
}
#[test]
fn crdt_error_converts() {
let crdt_err = nodedb_crdt::CrdtError::ConstraintViolation {
constraint: "test".into(),
collection: "col".into(),
detail: "detail".into(),
};
let e: Error = crdt_err.into();
assert!(matches!(e, Error::Crdt(_)));
}
#[test]
fn internal_error_to_nodedb_error() {
let e = Error::Wal(nodedb_wal::WalError::Sealed);
let public: NodeDbError = e.into();
assert!(public.is_storage());
assert!(public.to_string().contains("NDB-4100"));
}
#[test]
fn constraint_to_nodedb_error() {
let e = Error::RejectedConstraint {
collection: "users".into(),
constraint: "unique_email".into(),
detail: "dup".into(),
};
let public: NodeDbError = e.into();
assert!(public.is_constraint_violation());
}
#[test]
fn io_error_to_nodedb_error() {
let e = Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
let public: NodeDbError = e.into();
assert!(public.is_storage());
}
}