1use std::{
10 backtrace::{Backtrace, BacktraceStatus},
11 error::Error as StdError,
12 ffi::{CStr, c_char},
13 fmt, ptr,
14};
15
16#[cfg(any(test, feature = "test-hooks"))]
17use cudf::testing::device_safety;
18use cudf::{
19 DeviceId,
20 runtime::{DeviceFaultKind, DeviceFaultNativeStatus, report_device_fault},
21};
22use cugraph_sys::{
23 cugraph_allocation_source_t, cugraph_error_alignment, cugraph_error_allocation_source, cugraph_error_code_t,
24 cugraph_error_cuda_error, cugraph_error_domain_id, cugraph_error_failure_source, cugraph_error_failure_source_t,
25 cugraph_error_free, cugraph_error_generation, cugraph_error_member_id, cugraph_error_message,
26 cugraph_error_requested_bytes, cugraph_error_t,
27};
28
29#[repr(C)]
30pub(crate) struct EdgeCountErrorDetail {
31 allocation_source: cugraph_allocation_source_t,
32 failure_source: cugraph_error_failure_source_t,
33 domain_id: u64,
34 member_id: u64,
35 generation: u64,
36 requested_bytes: u64,
37 alignment: u64,
38 cuda_error: i32,
39 message: [c_char; 512],
40}
41
42impl EdgeCountErrorDetail {
43 pub(crate) fn new() -> Self {
44 Self {
45 allocation_source: cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_UNKNOWN,
46 failure_source: cugraph_error_failure_source_t::CUGRAPH_ERROR_FAILURE_SOURCE_UNKNOWN,
47 domain_id: 0,
48 member_id: 0,
49 generation: 0,
50 requested_bytes: 0,
51 alignment: 0,
52 cuda_error: -1,
53 message: [0; 512],
54 }
55 }
56}
57
58const CUGRAPH_RS_CUDA_FAILURE_NONE: i32 = 0;
59const CUGRAPH_RS_CUDA_FAILURE_RECOVERABLE: i32 = 1;
60const CUGRAPH_RS_CUDA_FAILURE_FATAL: i32 = 2;
61
62unsafe extern "C" {
63 fn cugraph_rs_classify_cuda_failure_after_error(cuda_status: *mut i32) -> i32;
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67enum NativeCudaFailure {
68 None,
69 Recoverable(i32),
70 Fatal(i32),
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80#[non_exhaustive]
81pub enum ErrorKind {
82 Unexpected,
84 InvalidHandle,
86 OutOfMemory,
88 InvalidInput,
90 IoError,
92 InvalidState,
94 NotImplemented,
96 UnsupportedType,
98 NonConvergence,
100}
101
102impl ErrorKind {
103 pub fn from_error_code(code: cugraph_error_code_t) -> Self {
105 match code {
106 cugraph_error_code_t::CUGRAPH_INVALID_HANDLE => ErrorKind::InvalidHandle,
107 cugraph_error_code_t::CUGRAPH_ALLOC_ERROR => ErrorKind::OutOfMemory,
108 cugraph_error_code_t::CUGRAPH_INVALID_INPUT => ErrorKind::InvalidInput,
109 cugraph_error_code_t::CUGRAPH_INVALID_STATE => ErrorKind::InvalidState,
110 cugraph_error_code_t::CUGRAPH_NOT_IMPLEMENTED => ErrorKind::NotImplemented,
111 cugraph_error_code_t::CUGRAPH_UNSUPPORTED_TYPE_COMBINATION => ErrorKind::UnsupportedType,
112 _ => ErrorKind::Unexpected,
113 }
114 }
115
116 pub fn from_error_code_raw(raw: u32) -> Self {
118 Self::from_error_code(cugraph_error_code_t(raw))
119 }
120
121 fn capture_backtrace(&self) -> bool {
127 matches!(self, ErrorKind::Unexpected)
128 }
129}
130
131impl fmt::Display for ErrorKind {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 ErrorKind::Unexpected => write!(f, "Unexpected"),
135 ErrorKind::InvalidHandle => write!(f, "InvalidHandle"),
136 ErrorKind::OutOfMemory => write!(f, "OutOfMemory"),
137 ErrorKind::InvalidInput => write!(f, "InvalidInput"),
138 ErrorKind::IoError => write!(f, "IoError"),
139 ErrorKind::InvalidState => write!(f, "InvalidState"),
140 ErrorKind::NotImplemented => write!(f, "NotImplemented"),
141 ErrorKind::UnsupportedType => write!(f, "UnsupportedType"),
142 ErrorKind::NonConvergence => write!(f, "NonConvergence"),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
154pub enum AllocationSource {
155 #[default]
157 Unknown,
158 RmmOutOfMemory,
160 RmmBadAlloc,
162 StdBadAlloc,
164}
165
166impl AllocationSource {
167 fn from_native(value: cugraph_allocation_source_t) -> Self {
168 match value {
169 cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_RMM_OUT_OF_MEMORY => Self::RmmOutOfMemory,
170 cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_RMM_BAD_ALLOC => Self::RmmBadAlloc,
171 cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_STD_BAD_ALLOC => Self::StdBadAlloc,
172 _ => Self::Unknown,
173 }
174 }
175
176 unsafe fn from_error(error: *const cugraph_error_t) -> Self {
177 let value = unsafe { cugraph_error_allocation_source(error) };
180 Self::from_native(value)
181 }
182
183 pub const fn as_str(self) -> &'static str {
185 match self {
186 Self::Unknown => "unknown",
187 Self::RmmOutOfMemory => "rmm_out_of_memory",
188 Self::RmmBadAlloc => "rmm_bad_alloc",
189 Self::StdBadAlloc => "std_bad_alloc",
190 }
191 }
192}
193
194impl fmt::Display for AllocationSource {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 f.write_str(self.as_str())
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
206pub enum AllocationFailureSource {
207 #[default]
209 Unknown,
210 ReservationLimit,
212 Physical,
214}
215
216impl AllocationFailureSource {
217 fn from_native(value: cugraph_error_failure_source_t) -> Self {
218 match value {
219 cugraph_error_failure_source_t::CUGRAPH_ERROR_FAILURE_SOURCE_PHYSICAL => Self::Physical,
220 cugraph_error_failure_source_t::CUGRAPH_ERROR_FAILURE_SOURCE_RESERVATION_LIMIT => Self::ReservationLimit,
221 _ => Self::Unknown,
222 }
223 }
224
225 unsafe fn from_error(error: *const cugraph_error_t) -> Self {
226 let value = unsafe { cugraph_error_failure_source(error) };
229 Self::from_native(value)
230 }
231
232 pub const fn as_str(self) -> &'static str {
234 match self {
235 Self::Unknown => "unknown",
236 Self::ReservationLimit => "reservation_limit",
237 Self::Physical => "physical",
238 }
239 }
240}
241
242impl fmt::Display for AllocationFailureSource {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 f.write_str(self.as_str())
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
264pub enum ErrorStatus {
265 Permanent,
268 Temporary,
271 Persistent,
275}
276
277impl fmt::Display for ErrorStatus {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 match self {
280 ErrorStatus::Permanent => write!(f, "permanent"),
281 ErrorStatus::Temporary => write!(f, "temporary"),
282 ErrorStatus::Persistent => write!(f, "persistent"),
283 }
284 }
285}
286
287#[derive(Debug, Clone, Copy)]
300struct AllocationFacts {
301 source: AllocationSource,
302 failure_source: AllocationFailureSource,
303 domain_id: u64,
304 member_id: u64,
305 generation: u64,
306 requested_bytes: u64,
307 alignment: u64,
308 cuda_error: i32,
309}
310
311impl Default for AllocationFacts {
312 fn default() -> Self {
313 Self {
314 source: AllocationSource::Unknown,
315 failure_source: AllocationFailureSource::Unknown,
316 domain_id: 0,
317 member_id: 0,
318 generation: 0,
319 requested_bytes: 0,
320 alignment: 0,
321 cuda_error: -1,
322 }
323 }
324}
325
326pub struct Error {
327 kind: ErrorKind,
328 status: ErrorStatus,
329 allocation: Option<Box<AllocationFacts>>,
330 operation: &'static str,
331 message: String,
332 context: Vec<(&'static str, String)>,
333 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
334 backtrace: Option<Box<Backtrace>>,
335}
336
337impl Error {
338 pub fn new(kind: ErrorKind, operation: &'static str) -> Self {
344 Self {
345 kind,
346 status: ErrorStatus::Permanent,
347 allocation: None,
348 operation,
349 message: String::new(),
350 context: Vec::new(),
351 source: None,
352 backtrace: kind
353 .capture_backtrace()
354 .then(Backtrace::capture)
355 .filter(|bt| bt.status() == BacktraceStatus::Captured)
356 .map(Box::new),
357 }
358 }
359
360 fn allocation_facts(&self) -> AllocationFacts {
361 self.allocation.as_deref().copied().unwrap_or_default()
362 }
363
364 pub fn with_message(mut self, message: impl Into<String>) -> Self {
366 self.message = message.into();
367 self
368 }
369
370 pub fn with_operation(mut self, operation: &'static str) -> Self {
376 if !self.operation.is_empty() {
377 self.context.push(("called", self.operation.to_string()));
378 }
379 self.operation = operation;
380 self
381 }
382
383 pub fn with_context(mut self, key: &'static str, value: impl Into<String>) -> Self {
385 self.context.push((key, value.into()));
386 self
387 }
388
389 pub fn set_source<E>(mut self, source: E) -> Self
394 where E: StdError + Send + Sync + 'static {
395 debug_assert!(self.source.is_none(), "the source error has been set");
396 self.source = Some(Box::new(source));
397 self
398 }
399
400 pub fn set_temporary(mut self) -> Self {
405 self.status = ErrorStatus::Temporary;
406 self
407 }
408
409 pub fn set_permanent(mut self) -> Self {
412 self.status = ErrorStatus::Permanent;
413 self
414 }
415
416 pub fn set_persistent(mut self) -> Self {
422 self.status = ErrorStatus::Persistent;
423 self
424 }
425
426 pub fn kind(&self) -> ErrorKind {
428 self.kind
429 }
430
431 pub fn status(&self) -> ErrorStatus {
433 self.status
434 }
435
436 pub fn allocation_source(&self) -> AllocationSource {
442 self.allocation_facts().source
443 }
444
445 pub fn allocation_failure_source(&self) -> AllocationFailureSource {
447 self.allocation_facts().failure_source
448 }
449
450 #[cfg(any(test, feature = "test-hooks"))]
456 #[must_use]
457 pub fn with_allocation_failure_source(mut self, failure_source: AllocationFailureSource) -> Self {
458 let mut facts = self.allocation_facts();
459 facts.failure_source = failure_source;
460 self.allocation = Some(Box::new(facts));
461 self
462 }
463
464 pub fn allocation_domain_id(&self) -> u64 {
466 self.allocation_facts().domain_id
467 }
468
469 pub fn allocation_member_id(&self) -> u64 {
471 self.allocation_facts().member_id
472 }
473
474 pub fn allocation_generation(&self) -> u64 {
476 self.allocation_facts().generation
477 }
478
479 pub fn allocation_requested_bytes(&self) -> u64 {
481 self.allocation_facts().requested_bytes
482 }
483
484 pub fn allocation_alignment(&self) -> u64 {
486 self.allocation_facts().alignment
487 }
488
489 pub fn allocation_cuda_error(&self) -> i32 {
491 self.allocation_facts().cuda_error
492 }
493
494 pub fn is_temporary(&self) -> bool {
499 self.status == ErrorStatus::Temporary
500 }
501
502 pub fn is_permanent(&self) -> bool {
504 self.status == ErrorStatus::Permanent
505 }
506
507 pub fn is_persistent(&self) -> bool {
512 self.status == ErrorStatus::Persistent
513 }
514
515 pub fn operation(&self) -> &'static str {
517 self.operation
518 }
519
520 pub fn message(&self) -> &str {
522 &self.message
523 }
524
525 pub fn context_value(&self, key: &str) -> Option<&str> {
527 self.context.iter().rev().find(|(candidate, _)| *candidate == key).map(|(_, value)| value.as_str())
528 }
529}
530
531impl fmt::Debug for Error {
532 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533 if f.alternate() {
534 let allocation = self.allocation_facts();
536 let mut d = f.debug_struct("Error");
537 d.field("kind", &self.kind);
538 d.field("status", &self.status);
539 d.field("allocation_source", &allocation.source);
540 d.field("allocation_failure_source", &allocation.failure_source);
541 d.field("allocation_domain_id", &allocation.domain_id);
542 d.field("allocation_member_id", &allocation.member_id);
543 d.field("allocation_generation", &allocation.generation);
544 d.field("allocation_requested_bytes", &allocation.requested_bytes);
545 d.field("allocation_alignment", &allocation.alignment);
546 d.field("allocation_cuda_error", &allocation.cuda_error);
547 d.field("operation", &self.operation);
548 if !self.message.is_empty() {
549 d.field("message", &self.message);
550 }
551 if !self.context.is_empty() {
552 d.field("context", &self.context);
553 }
554 if let Some(ref src) = self.source {
555 d.field("source", src);
556 }
557 d.finish()
558 } else {
559 write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
561 if !self.message.is_empty() {
562 write!(f, " => {}", self.message)?;
563 }
564 if !self.context.is_empty() {
565 write!(f, "\n\nContext:")?;
566 for (key, value) in &self.context {
567 write!(f, "\n {}: {}", key, value)?;
568 }
569 }
570 if let Some(ref src) = self.source {
571 write!(f, "\n\nSource: {:#}", src)?;
572 }
573 if let Some(ref bt) = self.backtrace {
574 write!(f, "\n\nBacktrace:\n{}", bt)?;
575 }
576 Ok(())
577 }
578 }
579}
580
581impl fmt::Display for Error {
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 write!(f, "{} ({}) at {}", self.kind, self.status, self.operation)?;
584 if !self.message.is_empty() {
585 write!(f, " => {}", self.message)?;
586 }
587 if !self.context.is_empty() {
588 write!(f, ", context: {{")?;
589 for (i, (key, value)) in self.context.iter().enumerate() {
590 if i > 0 {
591 write!(f, ",")?;
592 }
593 write!(f, " {}: {}", key, value)?;
594 }
595 write!(f, " }}")?;
596 }
597 if let Some(ref src) = self.source {
598 write!(f, ", source: {}", src)?;
599 }
600 Ok(())
601 }
602}
603
604impl std::error::Error for Error {
605 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
606 self.source.as_ref().map(|e| e.as_ref() as &(dyn StdError + 'static))
607 }
608}
609
610pub type Result<T> = std::result::Result<T, Error>;
612
613pub(crate) fn unexpected_null_pointer(operation: &'static str, pointer_name: &'static str) -> Error {
614 Error::new(ErrorKind::Unexpected, operation)
615 .with_message(format!("C API returned null pointer for {pointer_name}"))
616 .with_context("pointer", pointer_name)
617}
618
619pub(crate) fn require_non_null<T>(ptr: *mut T, operation: &'static str, pointer_name: &'static str) -> Result<*mut T> {
620 if ptr.is_null() { Err(unexpected_null_pointer(operation, pointer_name)) } else { Ok(ptr) }
621}
622
623pub(crate) unsafe fn check_error(
638 code: cugraph_error_code_t,
639 error_ptr: *mut cugraph_error_t,
640 device: i32,
641 operation: &'static str,
642) -> Result<()> {
643 if code == cugraph_error_code_t::CUGRAPH_SUCCESS {
644 return Ok(());
645 }
646
647 let payload = unsafe { copy_native_error_payload(error_ptr) };
653 let cuda_failure = unsafe { native_cuda_failure_after_error() };
656 unsafe { check_error_with_payload(code, error_ptr, payload, Some((device, cuda_failure)), operation) }
659}
660
661pub(crate) unsafe fn check_edge_count_error(
668 code: cugraph_error_code_t,
669 detail: &EdgeCountErrorDetail,
670 device: i32,
671 operation: &'static str,
672) -> Result<()> {
673 if code == cugraph_error_code_t::CUGRAPH_SUCCESS {
674 return Ok(());
675 }
676 let payload = NativeErrorPayload {
677 allocation_source: AllocationSource::from_native(detail.allocation_source),
678 allocation_failure_source: AllocationFailureSource::from_native(detail.failure_source),
679 domain_id: detail.domain_id,
680 member_id: detail.member_id,
681 generation: detail.generation,
682 requested_bytes: detail.requested_bytes,
683 alignment: detail.alignment,
684 cuda_error: detail.cuda_error,
685 message: unsafe { CStr::from_ptr(detail.message.as_ptr()) }.to_string_lossy().into_owned(),
687 };
688 unsafe {
690 let cuda_failure = native_cuda_failure_after_error();
691 check_error_with_payload(code, ptr::null_mut(), payload, Some((device, cuda_failure)), operation)
692 }
693}
694
695pub(crate) unsafe fn check_host_error(
702 code: cugraph_error_code_t,
703 error_ptr: *mut cugraph_error_t,
704 operation: &'static str,
705) -> Result<()> {
706 if code == cugraph_error_code_t::CUGRAPH_SUCCESS {
707 return Ok(());
708 }
709 let payload = unsafe { copy_native_error_payload(error_ptr) };
712 unsafe { check_error_with_payload(code, error_ptr, payload, None, operation) }
715}
716
717unsafe fn native_cuda_failure_after_error() -> NativeCudaFailure {
718 let mut cuda_status = 0;
719 let classification = unsafe { cugraph_rs_classify_cuda_failure_after_error(&mut cuda_status) };
722 match classification {
723 CUGRAPH_RS_CUDA_FAILURE_NONE => NativeCudaFailure::None,
724 CUGRAPH_RS_CUDA_FAILURE_FATAL => NativeCudaFailure::Fatal(cuda_status),
725 CUGRAPH_RS_CUDA_FAILURE_RECOVERABLE => NativeCudaFailure::Recoverable(cuda_status),
726 _ => NativeCudaFailure::Recoverable(cuda_status),
727 }
728}
729
730struct NativeErrorPayload {
731 allocation_source: AllocationSource,
732 allocation_failure_source: AllocationFailureSource,
733 domain_id: u64,
734 member_id: u64,
735 generation: u64,
736 requested_bytes: u64,
737 alignment: u64,
738 cuda_error: i32,
739 message: String,
740}
741
742impl NativeErrorPayload {
743 fn unknown() -> Self {
744 Self {
745 allocation_source: AllocationSource::Unknown,
746 allocation_failure_source: AllocationFailureSource::Unknown,
747 domain_id: 0,
748 member_id: 0,
749 generation: 0,
750 requested_bytes: 0,
751 alignment: 0,
752 cuda_error: -1,
753 message: String::new(),
754 }
755 }
756}
757
758unsafe fn copy_native_error_payload(error: *const cugraph_error_t) -> NativeErrorPayload {
759 if error.is_null() {
760 return NativeErrorPayload::unknown();
761 }
762
763 let message = unsafe { cugraph_error_message(error) };
766 let message = if message.is_null() {
767 String::new()
768 } else {
769 unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned()
771 };
772
773 let (
776 allocation_source,
777 allocation_failure_source,
778 domain_id,
779 member_id,
780 generation,
781 requested_bytes,
782 alignment,
783 cuda_error,
784 ) = unsafe {
785 (
786 AllocationSource::from_error(error),
787 AllocationFailureSource::from_error(error),
788 cugraph_error_domain_id(error),
789 cugraph_error_member_id(error),
790 cugraph_error_generation(error),
791 cugraph_error_requested_bytes(error),
792 cugraph_error_alignment(error),
793 cugraph_error_cuda_error(error),
794 )
795 };
796
797 NativeErrorPayload {
798 allocation_source,
799 allocation_failure_source,
800 domain_id,
801 member_id,
802 generation,
803 requested_bytes,
804 alignment,
805 cuda_error,
806 message,
807 }
808}
809
810unsafe fn check_error_with_payload(
811 code: cugraph_error_code_t,
812 error_ptr: *mut cugraph_error_t,
813 payload: NativeErrorPayload,
814 device_failure: Option<(i32, NativeCudaFailure)>,
815 operation: &'static str,
816) -> Result<()> {
817 let kind = ErrorKind::from_error_code(code);
818 let mut err = Error::new(kind, operation);
819 err.allocation = Some(Box::new(AllocationFacts {
820 source: payload.allocation_source,
821 failure_source: payload.allocation_failure_source,
822 domain_id: payload.domain_id,
823 member_id: payload.member_id,
824 generation: payload.generation,
825 requested_bytes: payload.requested_bytes,
826 alignment: payload.alignment,
827 cuda_error: payload.cuda_error,
828 }));
829
830 if kind == ErrorKind::OutOfMemory {
832 err = err.set_temporary();
833 }
834
835 if let Some((device, cuda_failure)) = device_failure {
836 match cuda_failure {
837 NativeCudaFailure::None => {},
838 NativeCudaFailure::Recoverable(cuda_status) => {
839 err = err
840 .with_context("cuda_failure_classification", "recoverable")
841 .with_context("cuda_status_code", cuda_status.to_string());
842 },
843 NativeCudaFailure::Fatal(cuda_status) => {
844 let physical = usize::try_from(device).ok().and_then(|ordinal| DeviceId::new(ordinal).ok());
845 #[cfg(any(test, feature = "test-hooks"))]
846 let physical = physical.or_else(|| device_safety::injected_device_id(device));
847 if let Some(device_id) = physical {
848 report_device_fault(
849 device_id,
850 DeviceFaultKind::FatalDeviceOperation,
851 operation,
852 Some(DeviceFaultNativeStatus::Cuda(cuda_status)),
853 );
854 }
855 err = err
856 .set_persistent()
857 .with_context("cuda_failure_classification", "fatal")
858 .with_context("cuda_status_code", cuda_status.to_string())
859 .with_context("device", device.to_string());
860 },
861 }
862 }
863
864 if !payload.message.is_empty() {
865 err = err.with_message(payload.message);
866 }
867
868 if !error_ptr.is_null() {
869 unsafe { cugraph_error_free(error_ptr) };
873 }
874
875 Err(err)
876}
877
878#[cfg(test)]
879mod tests {
880 use cudf::runtime::DeviceHealthStatus;
881
882 use super::*;
883
884 #[test]
885 fn allocation_source_maps_native_values_and_fails_closed() {
886 assert_eq!(
887 AllocationSource::from_native(cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_UNKNOWN),
888 AllocationSource::Unknown
889 );
890 assert_eq!(
891 AllocationSource::from_native(cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_RMM_OUT_OF_MEMORY),
892 AllocationSource::RmmOutOfMemory
893 );
894 assert_eq!(
895 AllocationSource::from_native(cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_RMM_BAD_ALLOC),
896 AllocationSource::RmmBadAlloc
897 );
898 assert_eq!(
899 AllocationSource::from_native(cugraph_allocation_source_t::CUGRAPH_ALLOCATION_SOURCE_STD_BAD_ALLOC),
900 AllocationSource::StdBadAlloc
901 );
902 assert_eq!(AllocationSource::from_native(cugraph_allocation_source_t(999)), AllocationSource::Unknown);
903 }
904
905 #[test]
906 fn allocation_failure_source_maps_native_values_and_fails_closed() {
907 assert_eq!(
908 AllocationFailureSource::from_native(cugraph_error_failure_source_t::CUGRAPH_ERROR_FAILURE_SOURCE_PHYSICAL),
909 AllocationFailureSource::Physical
910 );
911 assert_eq!(
912 AllocationFailureSource::from_native(
913 cugraph_error_failure_source_t::CUGRAPH_ERROR_FAILURE_SOURCE_RESERVATION_LIMIT
914 ),
915 AllocationFailureSource::ReservationLimit
916 );
917 assert_eq!(
918 AllocationFailureSource::from_native(cugraph_error_failure_source_t::CUGRAPH_ERROR_FAILURE_SOURCE_UNKNOWN),
919 AllocationFailureSource::Unknown
920 );
921 assert_eq!(
922 AllocationFailureSource::from_native(cugraph_error_failure_source_t(999)),
923 AllocationFailureSource::Unknown
924 );
925 }
926
927 #[test]
928 fn test_error_kind_from_error_code() {
929 assert_eq!(ErrorKind::from_error_code(cugraph_error_code_t::CUGRAPH_INVALID_HANDLE), ErrorKind::InvalidHandle);
930 assert_eq!(ErrorKind::from_error_code(cugraph_error_code_t::CUGRAPH_ALLOC_ERROR), ErrorKind::OutOfMemory);
931 assert_eq!(ErrorKind::from_error_code(cugraph_error_code_t::CUGRAPH_INVALID_INPUT), ErrorKind::InvalidInput);
932 assert_eq!(ErrorKind::from_error_code(cugraph_error_code_t::CUGRAPH_INVALID_STATE), ErrorKind::InvalidState);
933 assert_eq!(
934 ErrorKind::from_error_code(cugraph_error_code_t::CUGRAPH_NOT_IMPLEMENTED),
935 ErrorKind::NotImplemented
936 );
937 assert_eq!(
938 ErrorKind::from_error_code(cugraph_error_code_t::CUGRAPH_UNSUPPORTED_TYPE_COMBINATION),
939 ErrorKind::UnsupportedType
940 );
941 }
942
943 #[test]
944 fn test_error_display_with_message_and_context() {
945 let err = Error::new(ErrorKind::InvalidInput, "GraphBuilder::build")
946 .with_message("src and dst must have the same length")
947 .with_context("src_len", "10")
948 .with_context("dst_len", "5");
949
950 let display = err.to_string();
951 assert_eq!(
952 display,
953 "InvalidInput (permanent) at GraphBuilder::build \
954 => src and dst must have the same length, \
955 context: { src_len: 10, dst_len: 5 }"
956 );
957 }
958
959 #[test]
960 fn test_error_source_chain() {
961 let source = std::io::Error::other("GPU fault");
962 let err = Error::new(ErrorKind::Unexpected, "ResourceHandle::new").set_source(source);
963
964 let std_source = std::error::Error::source(&err);
965 assert!(std_source.is_some());
966 assert_eq!(std_source.unwrap().to_string(), "GPU fault");
967 }
968
969 #[test]
970 fn native_payload_preserves_allocation_facts_and_error_identity() {
971 let payload = NativeErrorPayload {
972 allocation_source: AllocationSource::RmmOutOfMemory,
973 allocation_failure_source: AllocationFailureSource::ReservationLimit,
974 domain_id: 11,
975 member_id: 17,
976 generation: 23,
977 requested_bytes: 4096,
978 alignment: 256,
979 cuda_error: 2,
980 message: "reservation limit".to_string(),
981 };
982
983 let err = unsafe {
986 check_error_with_payload(
987 cugraph_error_code_t::CUGRAPH_ALLOC_ERROR,
988 std::ptr::null_mut(),
989 payload,
990 None,
991 "allocation_op",
992 )
993 }
994 .expect_err("allocation payload must produce an error");
995
996 assert_eq!(err.kind(), ErrorKind::OutOfMemory);
997 assert_eq!(err.status(), ErrorStatus::Temporary);
998 assert_eq!(err.allocation_source(), AllocationSource::RmmOutOfMemory);
999 assert_eq!(err.allocation_failure_source(), AllocationFailureSource::ReservationLimit);
1000 assert_eq!(err.allocation_domain_id(), 11);
1001 assert_eq!(err.allocation_member_id(), 17);
1002 assert_eq!(err.allocation_generation(), 23);
1003 assert_eq!(err.allocation_requested_bytes(), 4096);
1004 assert_eq!(err.allocation_alignment(), 256);
1005 assert_eq!(err.allocation_cuda_error(), 2);
1006 assert_eq!(err.message(), "reservation limit");
1007 }
1008
1009 #[test]
1010 fn test_with_operation_stacking() {
1011 let err = Error::new(ErrorKind::InvalidInput, "inner_op").with_message("bad data").with_operation("outer_op");
1012
1013 assert_eq!(err.operation(), "outer_op");
1014 let display = err.to_string();
1015 assert!(display.contains("at outer_op"));
1016 assert!(display.contains("called: inner_op"));
1017 }
1018
1019 #[test]
1020 fn test_check_error_success() {
1021 unsafe {
1024 let result = check_host_error(cugraph_error_code_t::CUGRAPH_SUCCESS, std::ptr::null_mut(), "test");
1025 assert!(result.is_ok());
1026 }
1027 }
1028
1029 #[test]
1030 fn test_require_non_null_rejects_null_pointers() {
1031 let err = require_non_null::<u8>(std::ptr::null_mut(), "test_op", "result_ptr").expect_err("null must fail");
1032 assert_eq!(err.kind(), ErrorKind::Unexpected);
1033 assert_eq!(err.operation(), "test_op");
1034 assert!(err.message().contains("result_ptr"));
1035 }
1036
1037 #[test]
1038 fn test_check_error_failure_null_ptr() {
1039 unsafe {
1042 let result = check_host_error(cugraph_error_code_t::CUGRAPH_INVALID_INPUT, std::ptr::null_mut(), "test_op");
1043 assert!(result.is_err());
1044 let err = result.unwrap_err();
1045 assert_eq!(err.kind(), ErrorKind::InvalidInput);
1046 assert_eq!(err.operation(), "test_op");
1047 assert!(err.is_permanent());
1048 }
1049 }
1050
1051 #[test]
1052 fn test_check_error_failure_unknown_code_maps_to_unexpected() {
1053 unsafe {
1056 let result = check_host_error(cugraph_error_code_t(777), std::ptr::null_mut(), "test_op");
1057 assert!(result.is_err());
1058 let err = result.unwrap_err();
1059 assert_eq!(err.kind(), ErrorKind::Unexpected);
1060 assert_eq!(err.operation(), "test_op");
1061 }
1062 }
1063
1064 #[test]
1065 fn test_check_error_out_of_memory_is_temporary() {
1066 unsafe {
1069 let result = check_host_error(cugraph_error_code_t::CUGRAPH_ALLOC_ERROR, std::ptr::null_mut(), "alloc_op");
1070 assert!(result.is_err());
1071 let err = result.unwrap_err();
1072 assert_eq!(err.kind(), ErrorKind::OutOfMemory);
1073 assert!(err.is_temporary());
1074 assert_eq!(err.allocation_source(), AllocationSource::Unknown);
1075 }
1076 }
1077
1078 #[test]
1079 fn fatal_native_cuda_classification_quarantines_the_explicit_owner_device() {
1080 let scope = device_safety::DeviceSafetyTestScope::install([]);
1081 let device = device_safety::injected_device_id(7).expect("injected owner device");
1082
1083 let error = unsafe {
1087 check_error_with_payload(
1088 cugraph_error_code_t::CUGRAPH_UNKNOWN_ERROR,
1089 std::ptr::null_mut(),
1090 NativeErrorPayload::unknown(),
1091 Some((7, NativeCudaFailure::Fatal(700))),
1092 "test_fatal_cugraph_call",
1093 )
1094 }
1095 .expect_err("fatal CUDA classification must fail");
1096
1097 assert!(error.is_persistent());
1098 assert!(error.to_string().contains("cuda_failure_classification: fatal"));
1099 let snapshot = scope.health_snapshot(device);
1100 assert_eq!(snapshot.status(), DeviceHealthStatus::Unhealthy);
1101 assert_eq!(snapshot.fault_count(), 1);
1102 let first = snapshot.first_fault().expect("fatal fault");
1103 assert_eq!(first.kind(), DeviceFaultKind::FatalDeviceOperation);
1104 assert_eq!(first.operation(), "test_fatal_cugraph_call");
1105 assert_eq!(first.native_status(), Some(DeviceFaultNativeStatus::Cuda(700)));
1106 }
1107}