Skip to main content

nv_core/
error.rs

1//! Typed error hierarchy for the NextVision runtime.
2//!
3//! Every error is typed and informative — no opaque string errors in core flows.
4//! Each variant carries enough context for operational debugging.
5
6use crate::id::{FeedId, StageId};
7
8/// Top-level error enum encompassing all NextVision error categories.
9#[derive(Debug, thiserror::Error)]
10pub enum NvError {
11    /// Error originating from the media/source layer.
12    #[error("media error: {0}")]
13    Media(#[from] MediaError),
14
15    /// Error originating from a perception stage.
16    #[error("stage error: {0}")]
17    Stage(#[from] StageError),
18
19    /// Error from the temporal state system.
20    #[error("temporal error: {0}")]
21    Temporal(#[from] TemporalError),
22
23    /// Error from the view/PTZ system.
24    #[error("view error: {0}")]
25    View(#[from] ViewError),
26
27    /// Error from the runtime/orchestration layer.
28    #[error("runtime error: {0}")]
29    Runtime(#[from] RuntimeError),
30
31    /// Configuration error — returned at feed/runtime creation time.
32    #[error("config error: {0}")]
33    Config(#[from] ConfigError),
34}
35
36/// Errors from the media ingress layer (source connection, decoding).
37///
38/// `Clone` is derived so that the same typed error can be delivered to
39/// both the health-event path and the frame-sink callback without
40/// downgrading one copy to a lossy display string.
41///
42/// **Security:** The `Display` implementation redacts credentials from
43/// URLs and sanitizes untrusted backend strings. This means log output
44/// and health events never contain raw secrets.
45#[derive(Debug, Clone, thiserror::Error)]
46pub enum MediaError {
47    /// Failed to connect to the video source.
48    #[error("connection failed to `{redacted_url}`: {detail}", redacted_url = crate::security::redact_url(url))]
49    ConnectionFailed { url: String, detail: String },
50
51    /// Decoding a video frame failed.
52    #[error("decode failed: {detail}")]
53    DecodeFailed { detail: String },
54
55    /// End of stream reached (file sources).
56    #[error("end of stream")]
57    Eos,
58
59    /// Source timed out (no data received within deadline).
60    #[error("source timeout")]
61    Timeout,
62
63    /// The source format or codec is not supported.
64    #[error("unsupported: {detail}")]
65    Unsupported { detail: String },
66
67    /// An RTSP source with `RequireTls` policy was given a non-TLS URL.
68    #[error("insecure RTSP rejected by RequireTls policy (use rtsps:// or set AllowInsecure)")]
69    InsecureRtspRejected,
70
71    /// A `SourceSpec::Custom` pipeline was rejected by the security policy.
72    #[error(
73        "custom pipeline rejected: set CustomPipelinePolicy::AllowTrusted on the runtime builder to enable custom pipelines"
74    )]
75    CustomPipelineRejected,
76}
77
78/// Errors from perception stages.
79///
80/// `Clone` is derived so that stage errors can be broadcast through
81/// health-event channels without wrapping in `Arc`.
82#[derive(Debug, Clone, thiserror::Error)]
83pub enum StageError {
84    /// The stage's processing logic failed.
85    #[error("stage `{stage_id}` processing failed: {detail}")]
86    ProcessingFailed { stage_id: StageId, detail: String },
87
88    /// The stage ran out of a resource (GPU OOM, buffer limit, etc.).
89    #[error("stage `{stage_id}` resource exhausted")]
90    ResourceExhausted { stage_id: StageId },
91
92    /// The stage could not load its model or contact an external dependency.
93    #[error("stage `{stage_id}` model/dependency load failed: {detail}")]
94    ModelLoadFailed { stage_id: StageId, detail: String },
95}
96
97/// Errors from the temporal state system.
98#[derive(Debug, thiserror::Error)]
99pub enum TemporalError {
100    /// A referenced track was not found in the temporal store.
101    #[error("track not found: {0}")]
102    TrackNotFound(crate::id::TrackId),
103
104    /// The retention policy rejected an operation.
105    #[error("retention limit exceeded: {detail}")]
106    RetentionLimitExceeded { detail: String },
107}
108
109/// Errors from the view/PTZ system.
110#[derive(Debug, thiserror::Error)]
111pub enum ViewError {
112    /// The view state provider returned invalid data.
113    #[error("invalid motion report: {detail}")]
114    InvalidMotionReport { detail: String },
115
116    /// A transform computation failed (e.g., degenerate homography).
117    #[error("transform computation failed: {detail}")]
118    TransformFailed { detail: String },
119}
120
121/// Errors from the runtime/orchestration layer.
122#[derive(Debug, thiserror::Error)]
123pub enum RuntimeError {
124    /// The specified feed was not found.
125    #[error("feed not found: {feed_id}")]
126    FeedNotFound { feed_id: FeedId },
127
128    /// The runtime is already running.
129    #[error("runtime is already running")]
130    AlreadyRunning,
131
132    /// The feed is already paused.
133    #[error("feed is already paused")]
134    AlreadyPaused,
135
136    /// The feed is not paused.
137    #[error("feed is not paused")]
138    NotPaused,
139
140    /// Shutdown is in progress; new operations are rejected.
141    #[error("shutdown in progress")]
142    ShutdownInProgress,
143
144    /// The maximum number of concurrent feeds has been reached.
145    #[error("feed limit exceeded (max: {max})")]
146    FeedLimitExceeded { max: usize },
147
148    /// An internal lock is poisoned (a thread panicked while holding it).
149    #[error("internal registry lock poisoned")]
150    RegistryPoisoned,
151
152    /// Failed to spawn a feed worker thread.
153    #[error("thread spawn failed: {detail}")]
154    ThreadSpawnFailed { detail: String },
155}
156
157/// Configuration errors — returned at feed or runtime construction time.
158#[derive(Debug, thiserror::Error)]
159pub enum ConfigError {
160    /// The source specification is invalid.
161    #[error("invalid source: {detail}")]
162    InvalidSource { detail: String },
163
164    /// A policy configuration is invalid.
165    #[error("invalid policy: {detail}")]
166    InvalidPolicy { detail: String },
167
168    /// A required configuration field is missing.
169    #[error("missing required field: `{field}`")]
170    MissingRequired { field: &'static str },
171
172    /// `CameraMode` and `ViewStateProvider` are inconsistent.
173    ///
174    /// For example: `Observed` without a provider, or `Fixed` with a provider.
175    #[error("camera mode conflict: {detail}")]
176    CameraModeConflict { detail: String },
177
178    /// A capacity or depth value is zero (which would deadlock or panic).
179    #[error("invalid capacity: {field} must be > 0")]
180    InvalidCapacity { field: &'static str },
181
182    /// Stage capability validation failed.
183    #[error("stage validation failed: {detail}")]
184    StageValidation { detail: String },
185
186    /// A batch coordinator with this processor ID already exists.
187    #[error("duplicate batch processor id: {id}")]
188    DuplicateBatchProcessorId { id: StageId },
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn nv_error_from_media_error() {
197        let err: NvError = MediaError::Timeout.into();
198        assert!(matches!(err, NvError::Media(MediaError::Timeout)));
199        assert!(err.to_string().contains("timeout"));
200    }
201
202    #[test]
203    fn nv_error_from_config_error() {
204        let err: NvError = ConfigError::MissingRequired { field: "source" }.into();
205        assert!(matches!(
206            err,
207            NvError::Config(ConfigError::MissingRequired { .. })
208        ));
209        assert!(err.to_string().contains("source"));
210    }
211
212    #[test]
213    fn stage_error_includes_stage_id() {
214        let err = StageError::ProcessingFailed {
215            stage_id: StageId("detector"),
216            detail: "NaN output".into(),
217        };
218        let msg = err.to_string();
219        assert!(msg.contains("detector"));
220        assert!(msg.contains("NaN output"));
221    }
222
223    #[test]
224    fn runtime_error_display() {
225        let err = RuntimeError::FeedLimitExceeded { max: 64 };
226        assert!(err.to_string().contains("64"));
227    }
228
229    #[test]
230    fn media_error_is_clone() {
231        let err = MediaError::ConnectionFailed {
232            url: "rtsp://cam".into(),
233            detail: "timeout".into(),
234        };
235        let err2 = err.clone();
236        assert_eq!(err.to_string(), err2.to_string());
237    }
238}