Skip to main content

cuvs/
error.rs

1use std::{
2    backtrace::{Backtrace, BacktraceStatus},
3    error::Error as StdError,
4    ffi::CStr,
5    fmt,
6};
7
8use cudf::{
9    ErrorCode,
10    interop::contract::{InteropError, InteropErrorKind},
11};
12use cuvs_sys::{cuvsError_t, cuvsErrorKind_t};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum ErrorKind {
17    Unexpected,
18    InvalidInput,
19    InvalidState,
20    OutOfMemory,
21    Unsupported,
22}
23
24impl ErrorKind {
25    fn capture_backtrace(&self) -> bool {
26        matches!(self, ErrorKind::Unexpected)
27    }
28}
29
30impl fmt::Display for ErrorKind {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            ErrorKind::Unexpected => write!(f, "Unexpected"),
34            ErrorKind::InvalidInput => write!(f, "InvalidInput"),
35            ErrorKind::InvalidState => write!(f, "InvalidState"),
36            ErrorKind::OutOfMemory => write!(f, "OutOfMemory"),
37            ErrorKind::Unsupported => write!(f, "Unsupported"),
38        }
39    }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum ErrorStatus {
44    Permanent,
45    Temporary,
46    Persistent,
47}
48
49impl fmt::Display for ErrorStatus {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            ErrorStatus::Permanent => write!(f, "permanent"),
53            ErrorStatus::Temporary => write!(f, "temporary"),
54            ErrorStatus::Persistent => write!(f, "persistent"),
55        }
56    }
57}
58
59pub struct Error {
60    kind: ErrorKind,
61    status: ErrorStatus,
62    operation: &'static str,
63    message: String,
64    context: Vec<(&'static str, String)>,
65    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
66    backtrace: Option<Box<Backtrace>>,
67}
68
69impl Error {
70    pub fn new(kind: ErrorKind, operation: &'static str) -> Self {
71        Self {
72            kind,
73            status: ErrorStatus::Permanent,
74            operation,
75            message: String::new(),
76            context: Vec::new(),
77            source: None,
78            backtrace: kind
79                .capture_backtrace()
80                .then(Backtrace::capture)
81                .filter(|bt| bt.status() == BacktraceStatus::Captured)
82                .map(Box::new),
83        }
84    }
85
86    pub fn with_message(mut self, message: impl Into<String>) -> Self {
87        self.message = message.into();
88        self
89    }
90
91    pub fn with_operation(mut self, operation: &'static str) -> Self {
92        if !self.operation.is_empty() {
93            self.context.push(("called", self.operation.to_string()));
94        }
95        self.operation = operation;
96        self
97    }
98
99    pub fn with_context(mut self, key: &'static str, value: impl ToString) -> Self {
100        self.context.push((key, value.to_string()));
101        self
102    }
103
104    pub fn set_source<E>(mut self, source: E) -> Self
105    where E: StdError + Send + Sync + 'static {
106        debug_assert!(self.source.is_none(), "source already set");
107        self.source = Some(Box::new(source));
108        self
109    }
110
111    pub fn set_temporary(mut self) -> Self {
112        self.status = ErrorStatus::Temporary;
113        self
114    }
115
116    pub fn set_permanent(mut self) -> Self {
117        self.status = ErrorStatus::Permanent;
118        self
119    }
120
121    pub fn set_persistent(mut self) -> Self {
122        self.status = ErrorStatus::Persistent;
123        self
124    }
125
126    pub fn kind(&self) -> ErrorKind {
127        self.kind
128    }
129
130    pub fn status(&self) -> ErrorStatus {
131        self.status
132    }
133
134    pub fn operation(&self) -> &'static str {
135        self.operation
136    }
137
138    pub fn message(&self) -> &str {
139        &self.message
140    }
141
142    /// Structured diagnostic facts forwarded from cudf/interop/native error
143    /// contracts. Consumers must not parse [`message`](Self::message) or
144    /// [`Display`](fmt::Display) to classify an error.
145    pub fn context(&self) -> impl ExactSizeIterator<Item = (&'static str, &str)> {
146        self.context.iter().map(|(key, value)| (*key, value.as_str()))
147    }
148
149    pub fn context_value(&self, key: &str) -> Option<&str> {
150        self.context.iter().rev().find(|(candidate, _)| *candidate == key).map(|(_, value)| value.as_str())
151    }
152
153    pub fn is_temporary(&self) -> bool {
154        self.status == ErrorStatus::Temporary
155    }
156}
157
158impl fmt::Debug for Error {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        if f.alternate() {
161            let mut d = f.debug_struct("Error");
162            d.field("kind", &self.kind).field("status", &self.status).field("operation", &self.operation);
163            if !self.message.is_empty() {
164                d.field("message", &self.message);
165            }
166            if !self.context.is_empty() {
167                d.field("context", &self.context);
168            }
169            if let Some(source) = &self.source {
170                d.field("source", source);
171            }
172            d.finish()
173        } else {
174            write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
175            if !self.message.is_empty() {
176                write!(f, " => {}", self.message)?;
177            }
178            if !self.context.is_empty() {
179                write!(f, "\n\nContext:")?;
180                for (key, value) in &self.context {
181                    write!(f, "\n   {key}: {value}")?;
182                }
183            }
184            if let Some(source) = &self.source {
185                write!(f, "\n\nSource: {source:#}")?;
186            }
187            if let Some(backtrace) = &self.backtrace {
188                write!(f, "\n\nBacktrace:\n{backtrace}")?;
189            }
190            Ok(())
191        }
192    }
193}
194
195impl fmt::Display for Error {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
198        if !self.message.is_empty() {
199            write!(f, " => {}", self.message)?;
200        }
201        if !self.context.is_empty() {
202            write!(f, ", context: {{")?;
203            for (i, (key, value)) in self.context.iter().enumerate() {
204                if i > 0 {
205                    write!(f, ",")?;
206                }
207                write!(f, " {key}: {value}")?;
208            }
209            write!(f, " }}")?;
210        }
211        if let Some(source) = &self.source {
212            write!(f, ", source: {source}")?;
213        }
214        Ok(())
215    }
216}
217
218impl StdError for Error {
219    fn source(&self) -> Option<&(dyn StdError + 'static)> {
220        self.source.as_ref().map(|source| source.as_ref() as &(dyn StdError + 'static))
221    }
222}
223
224pub type Result<T> = std::result::Result<T, Error>;
225
226pub(crate) fn invalid_input(operation: &'static str, message: impl Into<String>) -> Error {
227    Error::new(ErrorKind::InvalidInput, operation).with_message(message)
228}
229
230pub(crate) fn invalid_state(operation: &'static str, message: impl Into<String>) -> Error {
231    Error::new(ErrorKind::InvalidState, operation).with_message(message)
232}
233
234pub(crate) fn unsupported(operation: &'static str, message: impl Into<String>) -> Error {
235    Error::new(ErrorKind::Unsupported, operation).with_message(message)
236}
237
238pub(crate) fn from_cudf(operation: &'static str, error: cudf::Error) -> Error {
239    let kind = match error.code() {
240        ErrorCode::OutOfMemory | ErrorCode::ForeignResourceExhausted => ErrorKind::OutOfMemory,
241        ErrorCode::UnsupportedDtype | ErrorCode::UnsupportedLayout | ErrorCode::UnsupportedOperation => {
242            ErrorKind::Unsupported
243        },
244        ErrorCode::EmptyConstructionRequiresRuntime
245        | ErrorCode::RuntimeBindingMismatch
246        | ErrorCode::ReadinessUnavailable
247        | ErrorCode::ExecutionBindingUnavailable
248        | ErrorCode::InvariantViolation
249        | ErrorCode::ForeignInvariantViolation => ErrorKind::InvalidState,
250        ErrorCode::CudaRuntime
251        | ErrorCode::FatalCuda
252        | ErrorCode::IoFailure
253        | ErrorCode::IoBusy
254        | ErrorCode::ForeignTransientDependencyFailure
255        | ErrorCode::ForeignPersistentDependencyFailure => ErrorKind::Unexpected,
256        _ => ErrorKind::InvalidInput,
257    };
258    let status = error.status();
259    let mut mapped = Error::new(kind, operation)
260        .with_message(error.message())
261        .with_context("cudf_code", error.code())
262        .with_context("cudf_kind", error.kind())
263        .with_context("cudf_status", status)
264        .with_context("cudf_operation", error.operation());
265    mapped = match status {
266        cudf::ErrorStatus::Permanent => mapped.set_permanent(),
267        cudf::ErrorStatus::Temporary => mapped.set_temporary(),
268        cudf::ErrorStatus::Persistent => mapped.set_persistent(),
269    };
270    mapped.set_source(error)
271}
272
273pub(crate) fn from_interop(operation: &'static str, error: InteropError) -> Error {
274    let kind = match error.kind() {
275        InteropErrorKind::UnsupportedType
276        | InteropErrorKind::UnsupportedLayout
277        | InteropErrorKind::PlacementUnsupported
278        | InteropErrorKind::ProfileUnsupported => ErrorKind::Unsupported,
279        InteropErrorKind::ExecutionBindingUnavailable
280        | InteropErrorKind::ExecutionBindingMismatch
281        | InteropErrorKind::ReadinessUnavailable
282        | InteropErrorKind::ReadinessMismatch => ErrorKind::InvalidState,
283        InteropErrorKind::NotFound | InteropErrorKind::InvalidView | InteropErrorKind::MixedDeviceUnsupported => {
284            ErrorKind::InvalidInput
285        },
286    };
287    let mut mapped = Error::new(kind, operation)
288        .with_message(error.message())
289        .with_context("interop_kind", format!("{:?}", error.kind()));
290    for (key, value) in error.context() {
291        mapped = mapped.with_context(key, value);
292    }
293    mapped.set_source(error)
294}
295
296pub(crate) fn require_nonzero(value: usize, operation: &'static str, name: &'static str) -> Result<usize> {
297    if value == 0 { Err(invalid_input(operation, format!("{name} must be greater than zero"))) } else { Ok(value) }
298}
299
300pub(crate) fn check_cuvs(status: cuvsError_t, operation: &'static str) -> Result<()> {
301    if status == cuvsError_t::CUVS_SUCCESS {
302        return Ok(());
303    }
304
305    let kind = unsafe { cuvs_sys::cuvsGetLastErrorKind() };
306    let message = unsafe {
307        let ptr = cuvs_sys::cuvsGetLastErrorText();
308        if ptr.is_null() {
309            "cuVS C API returned CUVS_ERROR without an error message".to_string()
310        } else {
311            CStr::from_ptr(ptr).to_string_lossy().into_owned()
312        }
313    };
314
315    Err(native_error(status, kind, operation, message))
316}
317
318fn native_error(
319    status: cuvsError_t,
320    native_kind: cuvsErrorKind_t,
321    operation: &'static str,
322    message: impl Into<String>,
323) -> Error {
324    let kind = match native_kind {
325        cuvsErrorKind_t::CUVS_ERROR_KIND_DEVICE_OUT_OF_MEMORY => ErrorKind::OutOfMemory,
326        // OutOfMemory projects to DeviceAllocationFailed, so host OOM remains a temporary dependency failure.
327        cuvsErrorKind_t::CUVS_ERROR_KIND_HOST_OUT_OF_MEMORY => ErrorKind::Unexpected,
328        cuvsErrorKind_t::CUVS_ERROR_KIND_NONE | cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN => ErrorKind::Unexpected,
329    };
330    let error = Error::new(kind, operation).with_message(message).with_context("native_status", format!("{status:?}"));
331    match native_kind {
332        cuvsErrorKind_t::CUVS_ERROR_KIND_DEVICE_OUT_OF_MEMORY | cuvsErrorKind_t::CUVS_ERROR_KIND_HOST_OUT_OF_MEMORY => {
333            error.set_temporary()
334        },
335        cuvsErrorKind_t::CUVS_ERROR_KIND_NONE | cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN => error,
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn native_message_text_never_changes_error_kind() {
345        for message in ["out of memory", "invalid argument", "unsupported dtype", "arbitrary localized native text"] {
346            let error =
347                native_error(cuvsError_t::CUVS_ERROR, cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN, "test", message);
348            assert_eq!(error.kind(), ErrorKind::Unexpected);
349            assert_eq!(error.message(), message);
350        }
351    }
352
353    #[test]
354    fn native_error_kind_classifies_allocations_without_parsing_text() {
355        for (native_kind, kind, status) in [
356            (cuvsErrorKind_t::CUVS_ERROR_KIND_DEVICE_OUT_OF_MEMORY, ErrorKind::OutOfMemory, ErrorStatus::Temporary),
357            (cuvsErrorKind_t::CUVS_ERROR_KIND_HOST_OUT_OF_MEMORY, ErrorKind::Unexpected, ErrorStatus::Temporary),
358            (cuvsErrorKind_t::CUVS_ERROR_KIND_NONE, ErrorKind::Unexpected, ErrorStatus::Permanent),
359            (cuvsErrorKind_t::CUVS_ERROR_KIND_UNKNOWN, ErrorKind::Unexpected, ErrorStatus::Permanent),
360        ] {
361            let error = native_error(cuvsError_t::CUVS_ERROR, native_kind, "test", "not an allocation message");
362            assert_eq!((error.kind(), error.status()), (kind, status));
363        }
364    }
365}