xerv_core/
error.rs

1//! Error types for XERV.
2//!
3//! This module provides strongly-typed errors with actionable context.
4//! All errors include relevant identifiers (trace ID, node ID, etc.) to
5//! aid in debugging and tracing.
6
7use crate::types::{ArenaOffset, NodeId, TraceId};
8use std::path::PathBuf;
9use thiserror::Error;
10
11/// The main error type for XERV operations.
12#[derive(Error, Debug)]
13pub enum XervError {
14    // =========================================================================
15    // Arena Errors (E001-E099)
16    // =========================================================================
17    /// Failed to create or open arena file.
18    #[error("E001: Failed to create arena at {path}: {cause}")]
19    ArenaCreate {
20        /// The path where arena creation failed.
21        path: PathBuf,
22        /// Reason for the failure.
23        cause: String,
24    },
25
26    /// Failed to memory-map the arena file.
27    #[error("E002: Failed to mmap arena at {path}: {cause}")]
28    ArenaMmap {
29        /// The path of the arena file.
30        path: PathBuf,
31        /// Reason for the mmap failure.
32        cause: String,
33    },
34
35    /// Arena write operation failed.
36    #[error("E003: Arena write failed at offset {offset} for trace {trace_id}: {cause}")]
37    ArenaWrite {
38        /// The trace identifier.
39        trace_id: TraceId,
40        /// The offset where write was attempted.
41        offset: ArenaOffset,
42        /// Reason for the write failure.
43        cause: String,
44    },
45
46    /// Arena read operation failed.
47    #[error("E004: Arena read failed at offset {offset}: {cause}")]
48    ArenaRead {
49        /// The offset that could not be read.
50        offset: ArenaOffset,
51        /// Reason for the read failure.
52        cause: String,
53    },
54
55    /// Arena capacity exceeded.
56    #[error(
57        "E005: Arena capacity exceeded: requested {requested} bytes, available {available} bytes"
58    )]
59    ArenaCapacity {
60        /// Number of bytes requested.
61        requested: u64,
62        /// Number of bytes available.
63        available: u64,
64    },
65
66    /// Invalid arena offset.
67    #[error("E006: Invalid arena offset {offset}: {cause}")]
68    ArenaInvalidOffset {
69        /// The invalid offset.
70        offset: ArenaOffset,
71        /// Reason why the offset is invalid.
72        cause: String,
73    },
74
75    /// Arena corruption detected.
76    #[error("E007: Arena corruption detected at offset {offset}: {cause}")]
77    ArenaCorruption {
78        /// The offset where corruption was detected.
79        offset: ArenaOffset,
80        /// Description of the corruption.
81        cause: String,
82    },
83
84    // =========================================================================
85    // Selector/Linker Errors (E100-E199)
86    // =========================================================================
87    /// Failed to resolve a selector expression.
88    #[error("E101: Selector resolution failed for '{selector}' in node {node_id}: {cause}")]
89    SelectorResolution {
90        /// The selector expression that failed to resolve.
91        selector: String,
92        /// The node where resolution was attempted.
93        node_id: NodeId,
94        /// Reason for the resolution failure.
95        cause: String,
96    },
97
98    /// Non-deterministic layout detected (unstable offsets).
99    #[error("E102: Non-deterministic layout for type '{type_name}': {cause}")]
100    NonDeterministicLayout {
101        /// The type with non-deterministic layout.
102        type_name: String,
103        /// Description of the layout issue.
104        cause: String,
105    },
106
107    /// Invalid selector syntax.
108    #[error("E103: Invalid selector syntax '{selector}': {cause}")]
109    SelectorSyntax {
110        /// The selector with invalid syntax.
111        selector: String,
112        /// Description of the syntax error.
113        cause: String,
114    },
115
116    /// Selector target not found.
117    #[error("E104: Selector target '{field}' not found in output of node {node_id}")]
118    SelectorTargetNotFound {
119        /// The field that was not found.
120        field: String,
121        /// The node whose output was queried.
122        node_id: NodeId,
123    },
124
125    /// Type mismatch in selector.
126    #[error("E105: Type mismatch for selector '{selector}': expected {expected}, got {actual}")]
127    SelectorTypeMismatch {
128        /// The selector expression with type mismatch.
129        selector: String,
130        /// The expected type.
131        expected: String,
132        /// The actual type found.
133        actual: String,
134    },
135
136    // =========================================================================
137    // Schema Evolution Errors (E110-E119)
138    // =========================================================================
139    /// Schema version not found.
140    #[error("E110: Schema version '{schema}@{version}' not found")]
141    SchemaVersionNotFound {
142        /// The schema name.
143        schema: String,
144        /// The requested version.
145        version: String,
146    },
147
148    /// Schema incompatible with no migration.
149    #[error("E111: Schema incompatible: {from} → {to}, no migration available")]
150    SchemaIncompatible {
151        /// The source schema version.
152        from: String,
153        /// The target schema version.
154        to: String,
155        /// Reason why schemas are incompatible.
156        cause: String,
157    },
158
159    /// Migration failed.
160    #[error("E112: Migration failed from {from} to {to}: {cause}")]
161    MigrationFailed {
162        /// The source schema version.
163        from: String,
164        /// The target schema version.
165        to: String,
166        /// Reason for the migration failure.
167        cause: String,
168    },
169
170    /// Breaking schema change without migration.
171    #[error("E113: Breaking change in {schema}: {change}")]
172    BreakingSchemaChange {
173        /// The affected schema.
174        schema: String,
175        /// Description of the breaking change.
176        change: String,
177    },
178
179    /// Migration path too long.
180    #[error("E114: Migration path from {from} to {to} exceeds max hops ({max_hops})")]
181    MigrationPathTooLong {
182        /// The source schema version.
183        from: String,
184        /// The target schema version.
185        to: String,
186        /// The maximum allowed number of migration hops.
187        max_hops: u32,
188    },
189
190    // =========================================================================
191    // Runtime/Dependency Errors (E200-E299)
192    // =========================================================================
193    /// Missing binary dependency.
194    #[error("E201: Missing binary '{binary}': {cause}")]
195    MissingBinary {
196        /// The name of the missing binary.
197        binary: String,
198        /// Reason why the binary is missing.
199        cause: String,
200    },
201
202    /// Runtime version mismatch.
203    #[error("E202: Runtime mismatch for '{runtime}': required {required}, found {found}")]
204    RuntimeMismatch {
205        /// The runtime with version mismatch.
206        runtime: String,
207        /// The required version.
208        required: String,
209        /// The found version.
210        found: String,
211    },
212
213    /// Container image unavailable.
214    #[error("E203: Container image unavailable: {image}")]
215    ContainerImageUnavailable {
216        /// The container image reference.
217        image: String,
218    },
219
220    /// Unsupported execution profile.
221    #[error("E204: Unsupported execution profile '{profile}' on this host")]
222    UnsupportedProfile {
223        /// The requested execution profile.
224        profile: String,
225    },
226
227    // =========================================================================
228    // Node Execution Errors (E300-E399)
229    // =========================================================================
230    /// Node execution failed.
231    #[error("E301: Node {node_id} execution failed in trace {trace_id}: {cause}")]
232    NodeExecution {
233        /// The node that failed to execute.
234        node_id: NodeId,
235        /// The trace in which execution occurred.
236        trace_id: TraceId,
237        /// Reason for the execution failure.
238        cause: String,
239    },
240
241    /// Node timeout.
242    #[error("E302: Node {node_id} timed out after {timeout_ms}ms in trace {trace_id}")]
243    NodeTimeout {
244        /// The node that timed out.
245        node_id: NodeId,
246        /// The trace in which timeout occurred.
247        trace_id: TraceId,
248        /// Timeout duration in milliseconds.
249        timeout_ms: u64,
250    },
251
252    /// Node panic.
253    #[error("E303: Node {node_id} panicked in trace {trace_id}: {message}")]
254    NodePanic {
255        /// The node that panicked.
256        node_id: NodeId,
257        /// The trace in which panic occurred.
258        trace_id: TraceId,
259        /// The panic message.
260        message: String,
261    },
262
263    /// Invalid node configuration.
264    #[error("E304: Invalid configuration for node {node_id}: {cause}")]
265    NodeConfig {
266        /// The node with invalid configuration.
267        node_id: NodeId,
268        /// Description of the configuration error.
269        cause: String,
270    },
271
272    /// Node not found.
273    #[error("E305: Node '{node_name}' not found in flow")]
274    NodeNotFound {
275        /// The name of the node that was not found.
276        node_name: String,
277    },
278
279    // =========================================================================
280    // Flow/Topology Errors (E400-E499)
281    // =========================================================================
282    /// No compatible version for migration.
283    #[error("E401: No compatible version found for trace {trace_id}: {cause}")]
284    NoCompatibleVersion {
285        /// The trace for which no compatible version was found.
286        trace_id: TraceId,
287        /// Reason why no compatible version is available.
288        cause: String,
289    },
290
291    /// Invalid flow topology.
292    #[error("E402: Invalid flow topology: {cause}")]
293    InvalidTopology {
294        /// Description of the topology issue.
295        cause: String,
296    },
297
298    /// Cycle detected without loop controller.
299    #[error("E403: Uncontrolled cycle detected involving nodes: {nodes:?}")]
300    UncontrolledCycle {
301        /// The nodes involved in the cycle.
302        nodes: Vec<NodeId>,
303    },
304
305    /// Invalid port reference.
306    #[error("E404: Invalid port '{port}' on node {node_id}")]
307    InvalidPort {
308        /// The port name that is invalid.
309        port: String,
310        /// The node with the invalid port.
311        node_id: NodeId,
312    },
313
314    /// Missing required edge.
315    #[error("E405: Missing required edge from {from_node}.{from_port} to {to_node}.{to_port}")]
316    MissingEdge {
317        /// The source node.
318        from_node: NodeId,
319        /// The source port.
320        from_port: String,
321        /// The destination node.
322        to_node: NodeId,
323        /// The destination port.
324        to_port: String,
325    },
326
327    /// Invalid edge (references non-existent node or port).
328    #[error("E406: Invalid edge from {from_node}.{from_port} to {to_node}.{to_port}")]
329    InvalidEdge {
330        /// The source node.
331        from_node: NodeId,
332        /// The source port.
333        from_port: String,
334        /// The destination node.
335        to_node: NodeId,
336        /// The destination port.
337        to_port: String,
338    },
339
340    // =========================================================================
341    // Pipeline Errors (E500-E599)
342    // =========================================================================
343    /// Pipeline not found.
344    #[error("E501: Pipeline '{pipeline_id}' not found")]
345    PipelineNotFound {
346        /// The pipeline identifier that was not found.
347        pipeline_id: String,
348    },
349
350    /// Pipeline already exists.
351    #[error("E502: Pipeline '{pipeline_id}' already exists")]
352    PipelineExists {
353        /// The pipeline identifier that already exists.
354        pipeline_id: String,
355    },
356
357    /// Pipeline concurrency limit reached.
358    #[error("E503: Pipeline '{pipeline_id}' concurrency limit reached: {current}/{max}")]
359    ConcurrencyLimit {
360        /// The pipeline identifier.
361        pipeline_id: String,
362        /// Current number of concurrent executions.
363        current: u32,
364        /// Maximum allowed concurrent executions.
365        max: u32,
366    },
367
368    /// Pipeline circuit breaker triggered.
369    #[error("E504: Pipeline '{pipeline_id}' circuit breaker open: error rate {error_rate:.1}%")]
370    CircuitBreakerOpen {
371        /// The pipeline identifier.
372        pipeline_id: String,
373        /// The error rate that triggered the circuit breaker.
374        error_rate: f64,
375    },
376
377    /// Pipeline drain timeout.
378    #[error("E505: Pipeline '{pipeline_id}' drain timeout: {pending_traces} traces still pending")]
379    DrainTimeout {
380        /// The pipeline identifier.
381        pipeline_id: String,
382        /// Number of traces still pending.
383        pending_traces: u32,
384    },
385
386    // =========================================================================
387    // WAL Errors (E600-E699)
388    // =========================================================================
389    /// WAL write failed.
390    #[error("E601: WAL write failed for trace {trace_id}: {cause}")]
391    WalWrite {
392        /// The trace being written to the WAL.
393        trace_id: TraceId,
394        /// Reason for the write failure.
395        cause: String,
396    },
397
398    /// WAL read failed.
399    #[error("E602: WAL read failed: {cause}")]
400    WalRead {
401        /// Reason for the read failure.
402        cause: String,
403    },
404
405    /// WAL corruption detected.
406    #[error("E603: WAL corruption detected at position {position}: {cause}")]
407    WalCorruption {
408        /// The position in the WAL where corruption was detected.
409        position: u64,
410        /// Description of the corruption.
411        cause: String,
412    },
413
414    /// WAL replay failed.
415    #[error("E604: WAL replay failed for trace {trace_id}: {cause}")]
416    WalReplay {
417        /// The trace being replayed from the WAL.
418        trace_id: TraceId,
419        /// Reason for the replay failure.
420        cause: String,
421    },
422
423    // =========================================================================
424    // WASM Errors (E700-E799)
425    // =========================================================================
426    /// WASM module loading failed.
427    #[error("E701: Failed to load WASM module '{module}': {cause}")]
428    WasmLoad {
429        /// The WASM module that failed to load.
430        module: String,
431        /// Reason for the load failure.
432        cause: String,
433    },
434
435    /// WASM execution failed.
436    #[error("E702: WASM execution failed in node {node_id}: {cause}")]
437    WasmExecution {
438        /// The node running WASM that failed.
439        node_id: NodeId,
440        /// Reason for the execution failure.
441        cause: String,
442    },
443
444    /// WASM memory allocation failed.
445    #[error("E703: WASM memory allocation failed: requested {requested} bytes")]
446    WasmMemoryAlloc {
447        /// Number of bytes requested for allocation.
448        requested: u64,
449    },
450
451    /// WASM host function error.
452    #[error("E704: WASM host function '{function}' failed: {cause}")]
453    WasmHostFunction {
454        /// The host function name that failed.
455        function: String,
456        /// Reason for the function failure.
457        cause: String,
458    },
459
460    // =========================================================================
461    // Configuration Errors (E800-E899)
462    // =========================================================================
463    /// YAML parsing failed.
464    #[error("E801: Failed to parse YAML at {path}: {cause}")]
465    YamlParse {
466        /// The path to the YAML file.
467        path: PathBuf,
468        /// Reason for the parse failure.
469        cause: String,
470    },
471
472    /// Invalid configuration value.
473    #[error("E802: Invalid configuration '{field}': {cause}")]
474    ConfigValue {
475        /// The configuration field with invalid value.
476        field: String,
477        /// Description of why the value is invalid.
478        cause: String,
479    },
480
481    /// Schema validation failed.
482    #[error("E803: Schema validation failed for '{schema}': {cause}")]
483    SchemaValidation {
484        /// The schema being validated.
485        schema: String,
486        /// Reason for the validation failure.
487        cause: String,
488    },
489
490    /// Serialization/deserialization error.
491    #[error("E804: Serialization error: {0}")]
492    Serialization(
493        /// The serialization error message.
494        String,
495    ),
496
497    // =========================================================================
498    // I/O Errors (E900-E999)
499    // =========================================================================
500    /// File I/O error.
501    #[error("E901: I/O error at {path}: {cause}")]
502    Io {
503        /// The path where the I/O error occurred.
504        path: PathBuf,
505        /// Description of the I/O error.
506        cause: String,
507    },
508
509    /// Network error.
510    #[error("E902: Network error: {cause}")]
511    Network {
512        /// Description of the network error.
513        cause: String,
514    },
515}
516
517impl XervError {
518    /// Get the error code (e.g., "E001").
519    #[must_use]
520    pub fn code(&self) -> &'static str {
521        match self {
522            Self::ArenaCreate { .. } => "E001",
523            Self::ArenaMmap { .. } => "E002",
524            Self::ArenaWrite { .. } => "E003",
525            Self::ArenaRead { .. } => "E004",
526            Self::ArenaCapacity { .. } => "E005",
527            Self::ArenaInvalidOffset { .. } => "E006",
528            Self::ArenaCorruption { .. } => "E007",
529            Self::SelectorResolution { .. } => "E101",
530            Self::NonDeterministicLayout { .. } => "E102",
531            Self::SelectorSyntax { .. } => "E103",
532            Self::SelectorTargetNotFound { .. } => "E104",
533            Self::SelectorTypeMismatch { .. } => "E105",
534            Self::SchemaVersionNotFound { .. } => "E110",
535            Self::SchemaIncompatible { .. } => "E111",
536            Self::MigrationFailed { .. } => "E112",
537            Self::BreakingSchemaChange { .. } => "E113",
538            Self::MigrationPathTooLong { .. } => "E114",
539            Self::MissingBinary { .. } => "E201",
540            Self::RuntimeMismatch { .. } => "E202",
541            Self::ContainerImageUnavailable { .. } => "E203",
542            Self::UnsupportedProfile { .. } => "E204",
543            Self::NodeExecution { .. } => "E301",
544            Self::NodeTimeout { .. } => "E302",
545            Self::NodePanic { .. } => "E303",
546            Self::NodeConfig { .. } => "E304",
547            Self::NodeNotFound { .. } => "E305",
548            Self::NoCompatibleVersion { .. } => "E401",
549            Self::InvalidTopology { .. } => "E402",
550            Self::UncontrolledCycle { .. } => "E403",
551            Self::InvalidPort { .. } => "E404",
552            Self::MissingEdge { .. } => "E405",
553            Self::PipelineNotFound { .. } => "E501",
554            Self::PipelineExists { .. } => "E502",
555            Self::ConcurrencyLimit { .. } => "E503",
556            Self::CircuitBreakerOpen { .. } => "E504",
557            Self::DrainTimeout { .. } => "E505",
558            Self::WalWrite { .. } => "E601",
559            Self::WalRead { .. } => "E602",
560            Self::WalCorruption { .. } => "E603",
561            Self::WalReplay { .. } => "E604",
562            Self::WasmLoad { .. } => "E701",
563            Self::WasmExecution { .. } => "E702",
564            Self::WasmMemoryAlloc { .. } => "E703",
565            Self::WasmHostFunction { .. } => "E704",
566            Self::YamlParse { .. } => "E801",
567            Self::ConfigValue { .. } => "E802",
568            Self::SchemaValidation { .. } => "E803",
569            Self::Serialization(_) => "E804",
570            Self::Io { .. } => "E901",
571            Self::Network { .. } => "E902",
572            Self::InvalidEdge { .. } => "E406",
573        }
574    }
575
576    /// Check if this error is retriable.
577    #[must_use]
578    pub fn is_retriable(&self) -> bool {
579        matches!(
580            self,
581            Self::ArenaWrite { .. }
582                | Self::NodeTimeout { .. }
583                | Self::WalWrite { .. }
584                | Self::Network { .. }
585        )
586    }
587
588    /// Check if this error is a configuration/validation error.
589    #[must_use]
590    pub fn is_config_error(&self) -> bool {
591        matches!(
592            self,
593            Self::SelectorResolution { .. }
594                | Self::NonDeterministicLayout { .. }
595                | Self::SelectorSyntax { .. }
596                | Self::SelectorTargetNotFound { .. }
597                | Self::SelectorTypeMismatch { .. }
598                | Self::SchemaVersionNotFound { .. }
599                | Self::BreakingSchemaChange { .. }
600                | Self::InvalidTopology { .. }
601                | Self::UncontrolledCycle { .. }
602                | Self::InvalidPort { .. }
603                | Self::MissingEdge { .. }
604                | Self::YamlParse { .. }
605                | Self::ConfigValue { .. }
606                | Self::SchemaValidation { .. }
607        )
608    }
609
610    /// Check if this error is a schema evolution error.
611    #[must_use]
612    pub fn is_schema_error(&self) -> bool {
613        matches!(
614            self,
615            Self::SchemaVersionNotFound { .. }
616                | Self::SchemaIncompatible { .. }
617                | Self::MigrationFailed { .. }
618                | Self::BreakingSchemaChange { .. }
619                | Self::MigrationPathTooLong { .. }
620                | Self::SchemaValidation { .. }
621        )
622    }
623}
624
625/// Result type alias using `XervError`.
626pub type Result<T> = std::result::Result<T, XervError>;
627
628/// Extension trait for adding context to errors.
629pub trait ResultExt<T> {
630    /// Add trace context to an error.
631    fn with_trace(self, trace_id: TraceId) -> Result<T>;
632
633    /// Add node context to an error.
634    fn with_node(self, node_id: NodeId) -> Result<T>;
635}
636
637impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
638    fn with_trace(self, trace_id: TraceId) -> Result<T> {
639        self.map_err(|e| XervError::NodeExecution {
640            node_id: NodeId::new(0),
641            trace_id,
642            cause: e.to_string(),
643        })
644    }
645
646    fn with_node(self, node_id: NodeId) -> Result<T> {
647        self.map_err(|e| XervError::NodeConfig {
648            node_id,
649            cause: e.to_string(),
650        })
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    #[test]
659    fn error_codes_are_correct() {
660        let err = XervError::ArenaCreate {
661            path: PathBuf::from("/tmp/test"),
662            cause: "test".to_string(),
663        };
664        assert_eq!(err.code(), "E001");
665
666        let err = XervError::SelectorResolution {
667            selector: "${test}".to_string(),
668            node_id: NodeId::new(1),
669            cause: "not found".to_string(),
670        };
671        assert_eq!(err.code(), "E101");
672    }
673
674    #[test]
675    fn error_display() {
676        let err = XervError::NodeTimeout {
677            node_id: NodeId::new(5),
678            trace_id: TraceId::new(),
679            timeout_ms: 5000,
680        };
681        let msg = format!("{}", err);
682        assert!(msg.contains("E302"));
683        assert!(msg.contains("node_5"));
684        assert!(msg.contains("5000ms"));
685    }
686
687    #[test]
688    fn retriable_errors() {
689        assert!(
690            XervError::Network {
691                cause: "timeout".to_string()
692            }
693            .is_retriable()
694        );
695
696        assert!(
697            !XervError::InvalidTopology {
698                cause: "cycle".to_string()
699            }
700            .is_retriable()
701        );
702    }
703
704    #[test]
705    fn config_errors() {
706        assert!(
707            XervError::YamlParse {
708                path: PathBuf::from("test.yaml"),
709                cause: "syntax".to_string()
710            }
711            .is_config_error()
712        );
713
714        assert!(
715            !XervError::NodeExecution {
716                node_id: NodeId::new(1),
717                trace_id: TraceId::new(),
718                cause: "failed".to_string()
719            }
720            .is_config_error()
721        );
722    }
723}