1use std::{borrow::Cow, error::Error as StdError, ffi::CStr, fmt, ptr::NonNull};
11
12use rapids_interop::{InteropError, InteropErrorKind};
13
14use crate::{core::memory::ReservationUsage, runtime};
15
16mod ffi;
17
18pub mod classification;
19pub mod code;
20pub mod facts;
21#[cfg(any(test, feature = "test-hooks"))]
22pub(crate) mod testing_hooks;
23
24pub use classification::{
25 CudfAllocationAttribution, CudfErrorClassification, allocation_attribution, classify_cudf_error,
26};
27pub use code::{ErrorCode, ErrorKind, ErrorStatus, kind_for_code, status_for_code};
28pub(crate) use facts::{
29 DeviceIdFact, FACT_ACTUAL_COUNT, FACT_ACTUAL_ROWS, FACT_COLUMN_INDEX, FACT_COLUMN_NAME,
30 FACT_CONTROL_PLANE_SCALAR_BYTES, FACT_DISTINCT_KEYS_COUNT, FACT_EXECUTION_BINDING_ID, FACT_EXPECTED_COUNT,
31 FACT_EXPECTED_ROWS, FACT_KEYS_COUNT, FACT_LEFT_DEVICE, FACT_LEFT_TYPE_ID, FACT_RIGHT_DEVICE, FACT_RIGHT_TYPE_ID,
32 FACT_TABLE_COLUMN_COUNT, FACT_TABLE_ROW_COUNT, FACT_TYPE_ID, FACT_VALIDATION_DEVICE, FACT_VALIDATION_REASON,
33 FACT_VALIDATION_STREAM, FactKey, IntoFactValue,
34};
35pub use facts::{FactKeyName, FactValue, Facts};
36pub use ffi::{CudfErrorDetail, CudfErrorFailureSource};
37
38type BoxedSourceError = Box<dyn StdError + Send + Sync + 'static>;
39
40pub struct Error {
42 inner: Box<ErrorInner>,
43}
44
45struct ErrorInner {
46 code: ErrorCode,
47 operation: &'static str,
48 message: String,
49 facts: Facts,
50 details: Vec<(&'static str, String)>,
51 budget_exhaustion: Option<BudgetExhaustionDetail>,
52 ffi_detail: Option<CudfErrorDetail>,
53 call_chain: Vec<&'static str>,
54 source: Option<BoxedSourceError>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct BudgetExhaustionDetail {
65 boundary: Cow<'static, str>,
66 operation: &'static str,
67 usage: Option<ReservationUsage>,
68}
69
70impl BudgetExhaustionDetail {
71 pub(crate) fn new(
72 boundary: impl Into<Cow<'static, str>>,
73 operation: &'static str,
74 usage: Option<ReservationUsage>,
75 ) -> Self {
76 Self { boundary: boundary.into(), operation, usage }
77 }
78
79 pub fn boundary(&self) -> &str {
81 &self.boundary
82 }
83
84 pub fn operation(&self) -> &'static str {
86 self.operation
87 }
88
89 pub fn usage(&self) -> Option<&ReservationUsage> {
91 self.usage.as_ref()
92 }
93}
94
95impl Error {
96 pub(crate) fn new_with(code: ErrorCode, operation: &'static str, message: impl Into<String>) -> Self {
103 let message = message.into();
104 assert!(!operation.is_empty(), "cudf internal error: operation label must not be empty");
105 assert!(!message.is_empty(), "cudf internal error: message must not be empty");
106 Self {
107 inner: Box::new(ErrorInner {
108 code,
109 operation,
110 message,
111 facts: Facts::new(),
112 details: Vec::new(),
113 budget_exhaustion: None,
114 ffi_detail: None,
115 call_chain: Vec::new(),
116 source: None,
117 }),
118 }
119 }
120
121 #[must_use]
123 pub(crate) fn with_fact<T>(mut self, key: FactKey<T>, value: T) -> Self
124 where T: IntoFactValue {
125 self.inner.facts.insert(key.name(), value.into_fact_value());
126 self
127 }
128
129 #[must_use]
131 pub(crate) fn with_column(self, name: impl Into<Cow<'static, str>>) -> Self {
132 self.with_fact(FACT_COLUMN_NAME, name.into())
133 }
134
135 #[must_use]
141 pub(crate) fn with_detail(mut self, key: &'static str, value: impl Into<String>) -> Self {
142 assert_canonical_key_not_used_as_detail(key);
143 self.inner.details.push((key, value.into()));
144 self
145 }
146
147 #[must_use]
149 pub(crate) fn with_fact_if_absent<T>(self, key: FactKey<T>, value: T) -> Self
150 where T: IntoFactValue {
151 if self.fact_value(key.name()).is_some() { self } else { self.with_fact(key, value) }
152 }
153
154 #[must_use]
157 pub(crate) fn with_detail_if_absent(self, key: &'static str, value: impl Into<String>) -> Self {
158 if self.detail_value(key).is_some() { self } else { self.with_detail(key, value) }
159 }
160
161 #[must_use]
166 pub(crate) fn with_details<I>(mut self, details: I) -> Self
167 where I: IntoIterator<Item = (&'static str, String)> {
168 let details = details.into_iter();
169 self.inner.details.reserve(details.size_hint().0);
170 for (key, value) in details {
171 assert_canonical_key_not_used_as_detail(key);
172 self.inner.details.push((key, value));
173 }
174 self
175 }
176
177 #[must_use]
179 pub(crate) fn with_budget_exhaustion_if_absent(mut self, detail: BudgetExhaustionDetail) -> Self {
180 if self.inner.budget_exhaustion.is_none() {
181 self.inner.budget_exhaustion = Some(detail);
182 }
183 self
184 }
185
186 #[must_use]
188 pub(crate) fn with_ffi_detail(mut self, detail: CudfErrorDetail) -> Self {
189 self.inner.ffi_detail = Some(detail);
190 self
191 }
192
193 #[must_use]
195 pub(crate) fn with_source(mut self, src: impl Into<BoxedSourceError>) -> Self {
196 self.inner.source = Some(src.into());
197 self
198 }
199
200 #[must_use]
205 pub(crate) fn reanchor(mut self, operation: &'static str) -> Self {
206 assert!(!operation.is_empty(), "cudf internal error: operation label must not be empty");
207 self.inner.call_chain.push(self.inner.operation);
208 self.inner.operation = operation;
209 self
210 }
211
212 #[must_use]
215 pub(crate) fn reanchor_if_needed(self, operation: &'static str) -> Self {
216 if self.operation() == operation { self } else { self.reanchor(operation) }
217 }
218
219 pub fn code(&self) -> ErrorCode {
221 self.inner.code
222 }
223
224 pub fn kind(&self) -> ErrorKind {
226 kind_for_code(self.inner.code)
227 }
228
229 pub fn status(&self) -> ErrorStatus {
231 status_for_code(self.inner.code)
232 }
233
234 pub fn operation(&self) -> &'static str {
236 self.inner.operation
237 }
238
239 pub fn message(&self) -> &str {
241 &self.inner.message
242 }
243
244 pub fn facts(&self) -> &Facts {
246 &self.inner.facts
247 }
248
249 pub fn fact_value(&self, key: FactKeyName) -> Option<&FactValue> {
251 self.inner.facts.get(key)
252 }
253
254 pub fn details(&self) -> &[(&'static str, String)] {
256 &self.inner.details
257 }
258
259 pub fn detail_value(&self, key: &str) -> Option<&str> {
261 self.inner.details.iter().find_map(|(k, v)| (*k == key).then_some(v.as_str()))
262 }
263
264 pub fn detail_values<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a str> + 'a {
266 self.inner.details.iter().filter_map(move |(k, v)| (*k == key).then_some(v.as_str()))
267 }
268
269 pub fn budget_exhaustion(&self) -> Option<&BudgetExhaustionDetail> {
271 self.inner.budget_exhaustion.as_ref()
272 }
273
274 pub fn ffi_detail(&self) -> Option<&CudfErrorDetail> {
276 self.inner.ffi_detail.as_ref()
277 }
278
279 pub fn call_chain(&self) -> &[&'static str] {
282 &self.inner.call_chain
283 }
284
285 pub fn source_error(&self) -> Option<&(dyn StdError + 'static)> {
287 self.inner.source.as_deref().map(|source| source as &(dyn StdError + 'static))
288 }
289
290 pub fn diagnostic_string(&self) -> String {
298 let mut out = format!(
299 "code={} kind={} status={} operation={} message={}",
300 self.code(),
301 self.kind(),
302 self.status(),
303 self.operation(),
304 diagnostic_quote(self.message()),
305 );
306
307 out.push_str(" facts={");
308 for (index, (key, value)) in self.facts().iter().enumerate() {
309 if index > 0 {
310 out.push_str(", ");
311 }
312 out.push_str(key.as_snake_case());
313 out.push('=');
314 out.push_str(&value.to_string());
315 }
316 out.push('}');
317
318 out.push_str(" details=[");
319 for (index, (key, value)) in self.details().iter().enumerate() {
320 if index > 0 {
321 out.push_str(", ");
322 }
323 out.push_str(key);
324 out.push('=');
325 out.push_str(&diagnostic_quote(value));
326 }
327 out.push(']');
328
329 if let Some(detail) = self.ffi_detail() {
330 out.push_str(" ffi_detail={");
331 out.push_str("status=");
332 out.push_str(&detail.status.to_string());
333 out.push_str(", failure_source=");
334 out.push_str(&detail.failure_source.to_string());
335 out.push_str(", domain_id=");
336 out.push_str(&detail.domain_id.to_string());
337 out.push_str(", member_id=");
338 out.push_str(&detail.member_id.to_string());
339 out.push_str(", generation=");
340 out.push_str(&detail.generation.to_string());
341 out.push_str(", requested_bytes=");
342 out.push_str(&detail.requested_bytes.to_string());
343 out.push_str(", alignment=");
344 out.push_str(&detail.alignment.to_string());
345 out.push_str(", cuda_error=");
346 out.push_str(&detail.cuda_error.to_string());
347 out.push_str(", message=");
348 out.push_str(&diagnostic_quote(&detail.message));
349 out.push('}');
350 }
351
352 out.push_str(" call_chain=[");
353 for (index, operation) in self.call_chain().iter().enumerate() {
354 if index > 0 {
355 out.push_str(" -> ");
356 }
357 out.push_str(operation);
358 }
359 out.push(']');
360
361 out.push_str(" source=");
362 match self.source_error() {
363 Some(source) => out.push_str(&diagnostic_quote(&source.to_string())),
364 None => out.push_str("none"),
365 }
366
367 out
368 }
369
370 pub fn is_retryable(&self) -> bool {
374 self.status() == ErrorStatus::Temporary
375 }
376
377 pub fn is_temporary(&self) -> bool {
379 self.status() == ErrorStatus::Temporary
380 }
381
382 pub fn is_permanent(&self) -> bool {
384 self.status() == ErrorStatus::Permanent
385 }
386
387 pub fn is_persistent(&self) -> bool {
389 self.status() == ErrorStatus::Persistent
390 }
391
392 pub fn is_oom(&self) -> bool {
397 self.inner.code == ErrorCode::OutOfMemory
398 }
399}
400
401impl fmt::Display for Error {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 write!(f, "{} ({}) at {}: {}", self.kind(), self.status(), self.inner.operation, self.inner.message)?;
406 for (k, v) in &self.inner.details {
407 write!(f, "; {}: {}", k, v)?;
408 }
409 if let Some(src) = &self.inner.source {
410 write!(f, "; source: {}", src)?;
411 }
412 Ok(())
413 }
414}
415
416impl fmt::Debug for Error {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 writeln!(f, "code: {}", self.inner.code)?;
419 writeln!(f, "kind: {} (derived)", self.kind())?;
420 writeln!(f, "status: {}", self.status())?;
421 writeln!(f, "operation: {}", self.inner.operation)?;
422 writeln!(f, "call_chain: {:?}", self.inner.call_chain)?;
423 writeln!(f, "facts: {}", self.inner.facts)?;
424 if let Some(detail) = &self.inner.ffi_detail {
425 writeln!(f, "ffi_detail: {:?}", detail)?;
426 }
427 for (k, v) in &self.inner.details {
428 writeln!(f, "{}: {}", k, v)?;
429 }
430 writeln!(f, "message: {}", self.inner.message)?;
431 writeln!(f, "source: {:?}", self.inner.source)?;
432 Ok(())
433 }
434}
435
436impl StdError for Error {
437 fn source(&self) -> Option<&(dyn StdError + 'static)> {
438 self.source_error()
439 }
440}
441
442pub type Result<T> = std::result::Result<T, Error>;
444
445fn assert_canonical_key_not_used_as_detail(key: &'static str) {
451 debug_assert!(
452 FactKeyName::from_snake_case(key).is_none(),
453 "cudf internal error: canonical fact key `{key}` written through the detail path; \
454 use `with_fact` instead",
455 );
456}
457
458fn diagnostic_quote(input: &str) -> String {
459 let mut out = String::with_capacity(input.len() + 2);
460 out.push('"');
461 for ch in input.chars() {
462 out.extend(ch.escape_default());
463 }
464 out.push('"');
465 out
466}
467
468pub(crate) fn invariant_violation(operation: &'static str, message: impl Into<String>) -> Error {
469 Error::new_with(ErrorCode::InvariantViolation, operation, message)
470}
471
472pub(crate) fn interior_null_byte_error(
481 operation: &'static str,
482 resource: &'static str,
483 message: &'static str,
484) -> Error {
485 Error::new_with(ErrorCode::NullUnexpected, operation, message)
486 .with_detail("resource", resource)
487 .with_detail("reason", "interior_null_byte")
488}
489
490pub(crate) fn row_count_mismatch_error(
492 operation: &'static str,
493 column_index: usize,
494 actual_rows: usize,
495 expected_rows: usize,
496) -> Error {
497 Error::new_with(
498 ErrorCode::ColumnLengthMismatch,
499 operation,
500 format!("column {column_index} has {actual_rows} rows but column 0 has {expected_rows} rows"),
501 )
502 .with_fact(FACT_COLUMN_INDEX, column_index)
503 .with_fact(FACT_ACTUAL_ROWS, actual_rows)
504 .with_fact(FACT_EXPECTED_ROWS, expected_rows)
505}
506
507pub(crate) fn limit_validation_error(
510 operation: &'static str,
511 message: impl Into<String>,
512 resource: &'static str,
513 value_key: &'static str,
514 limit: &'static str,
515 value: usize,
516 remedy: impl Into<String>,
517) -> Error {
518 let reason = match limit {
519 "i32::MAX" => "exceeds_i32_max",
520 "i64::MAX" => "exceeds_i64_max",
521 _ => "exceeds_supported_range",
522 };
523
524 let err = Error::new_with(ErrorCode::UnsupportedOperation, operation, message);
525 let err = match FactKeyName::from_snake_case(value_key) {
526 Some(FactKeyName::ColumnIndex) => err.with_fact(FACT_COLUMN_INDEX, value),
527 Some(FactKeyName::ExpectedRows) => err.with_fact(FACT_EXPECTED_ROWS, value),
528 Some(FactKeyName::ActualRows) => err.with_fact(FACT_ACTUAL_ROWS, value),
529 Some(FactKeyName::ExpectedCount) => err.with_fact(FACT_EXPECTED_COUNT, value),
530 Some(FactKeyName::ActualCount) => err.with_fact(FACT_ACTUAL_COUNT, value),
531 Some(FactKeyName::TableRowCount) => err.with_fact(FACT_TABLE_ROW_COUNT, value),
532 Some(FactKeyName::TableColumnCount) => err.with_fact(FACT_TABLE_COLUMN_COUNT, value),
533 Some(FactKeyName::KeysCount) => err.with_fact(FACT_KEYS_COUNT, value),
534 Some(FactKeyName::DistinctKeysCount) => err.with_fact(FACT_DISTINCT_KEYS_COUNT, value),
535 _ => err.with_detail(value_key, value.to_string()),
536 };
537
538 err.with_detail("resource", resource).with_detail("reason", reason).with_detail("remedy", remedy.into())
539}
540
541pub(crate) fn foreign_invariant_violation(
544 operation: &'static str,
545 message: impl Into<String>,
546 resource: &'static str,
547 reason: &'static str,
548 remedy: impl Into<String>,
549) -> Error {
550 Error::new_with(ErrorCode::ForeignInvariantViolation, operation, message)
551 .with_detail("resource", resource)
552 .with_detail("reason", reason)
553 .with_detail("remedy", remedy.into())
554}
555
556#[must_use]
559pub(crate) fn annotate_foreign_invariant_contract_if_absent(
560 mut err: Error,
561 resource: &'static str,
562 reason: &'static str,
563 remedy: impl Into<String>,
564) -> Error {
565 if matches!(err.code(), ErrorCode::ForeignInvariantViolation | ErrorCode::InvariantViolation) {
566 let remedy = remedy.into();
567 err = err.with_detail_if_absent("resource", resource);
568 err = err.with_detail_if_absent("reason", reason);
569 err = err.with_detail_if_absent("remedy", remedy);
570 }
571 err
572}
573
574pub(crate) fn range_limit_error(
576 operation: &'static str,
577 field: &'static str,
578 limit: &'static str,
579 value: usize,
580) -> Error {
581 limit_validation_error(
582 operation,
583 format!("{field} exceeds {limit}"),
584 field,
585 field,
586 limit,
587 value,
588 format!("reduce {field} to fit within {limit}"),
589 )
590}
591
592pub(crate) fn row_range_limit_error(operation: &'static str, bound: &'static str, value: usize) -> Error {
594 let (message, key, reason) = match bound {
595 "start" => ("row range start exceeds i32::MAX", "start_row", "start_exceeds_i32_max"),
596 "end" => ("row range end exceeds i32::MAX", "end_row", "end_exceeds_i32_max"),
597 _ => ("row range bound exceeds i32::MAX", "row_bound", "bound_exceeds_i32_max"),
598 };
599
600 Error::new_with(ErrorCode::UnsupportedOperation, operation, message)
601 .with_detail(key, value.to_string())
602 .with_detail("resource", "row_range")
603 .with_detail("reason", reason)
604 .with_detail("remedy", "reduce row range bounds to fit within i32::MAX")
605}
606
607pub(crate) fn row_range_overflow_error(operation: &'static str, start: usize, len: usize) -> Error {
610 let end_row = (start as u128) + (len as u128);
611
612 Error::new_with(ErrorCode::UnsupportedOperation, operation, "row range end overflowed usize")
613 .with_detail("start_row", start.to_string())
614 .with_detail("end_row", end_row.to_string())
615 .with_detail("resource", "row_range")
616 .with_detail("reason", "end_overflowed_usize")
617 .with_detail("remedy", "reduce row range bounds to fit within usize")
618}
619
620pub(crate) fn require_non_null<T>(ptr: *mut T, operation: &'static str, target: &'static str) -> Result<NonNull<T>> {
623 NonNull::new(ptr).ok_or_else(|| {
624 Error::new_with(
625 ErrorCode::InvariantViolation,
626 operation,
627 format!("FFI returned null {target} handle on success"),
628 )
629 .with_detail("target", target)
630 })
631}
632
633pub(crate) fn usize_to_i32(value: usize, operation: &'static str, field: &'static str) -> Result<i32> {
635 i32::try_from(value).map_err(|_| range_limit_error(operation, field, "i32::MAX", value))
636}
637
638fn leaf_code_for_status(status: cudf_sys::cudf_status_t) -> ErrorCode {
639 match status {
640 cudf_sys::CUDF_ERROR_INVALID_ARGUMENT | cudf_sys::CUDF_ERROR_INVALID_OPERATION | cudf_sys::CUDF_ERROR_TYPE => {
641 ErrorCode::ForeignInvalidArgument
642 },
643 cudf_sys::CUDF_ERROR_INVARIANT_VIOLATION => ErrorCode::ForeignInvariantViolation,
644 cudf_sys::CUDF_ERROR_RUNTIME_BINDING_MISMATCH => ErrorCode::RuntimeBindingMismatch,
645 cudf_sys::CUDF_ERROR_CUDA => ErrorCode::CudaRuntime,
646 cudf_sys::CUDF_ERROR_FATAL_CUDA => ErrorCode::FatalCuda,
647 cudf_sys::CUDF_ERROR_OUT_OF_MEMORY => ErrorCode::OutOfMemory,
648 cudf_sys::CUDF_ERROR_SIZE_OVERFLOW => ErrorCode::SizeOverflow,
649 cudf_sys::CUDF_ERROR_UNSUPPORTED => ErrorCode::UnsupportedOperation,
650 cudf_sys::CUDF_ERROR_IO => ErrorCode::IoFailure,
651 cudf_sys::CUDF_ERROR_NULL => ErrorCode::NullUnexpected,
652 cudf_sys::CUDF_ERROR_INDEX => ErrorCode::IndexOutOfBounds,
653 _ => ErrorCode::ForeignInvariantViolation,
654 }
655}
656
657#[cfg(any(test, feature = "test-hooks"))]
658fn error_for_ffi_status(status: cudf_sys::cudf_status_t, operation: &'static str, message: String) -> Error {
659 error_for_ffi_status_with_detail(status, operation, message, None)
660}
661
662fn error_for_ffi_status_with_detail(
663 status: cudf_sys::cudf_status_t,
664 operation: &'static str,
665 message: String,
666 ffi_detail: Option<CudfErrorDetail>,
667) -> Error {
668 let error = Error::new_with(leaf_code_for_status(status), operation, message);
669 match ffi_detail {
670 Some(detail) => error.with_ffi_detail(detail),
671 None => error,
672 }
673}
674
675fn read_ffi_last_error_message() -> String {
676 unsafe {
680 let ptr = cudf_sys::cudf_last_error_message();
681 if ptr.is_null() {
682 String::from("(no error message)")
683 } else {
684 CStr::from_ptr(ptr).to_string_lossy().into_owned()
685 }
686 }
687}
688
689fn read_ffi_error(status: cudf_sys::cudf_status_t) -> (String, Option<CudfErrorDetail>) {
690 unsafe {
694 let detail =
695 cudf_sys::cudf_last_error_detail().as_ref().map(|detail| CudfErrorDetail::from_raw(detail, status));
696 match detail {
697 Some(detail) if !detail.message.is_empty() => (detail.message.clone(), Some(detail)),
698 Some(detail) => (String::from("(no error message)"), Some(detail)),
699 None => (read_ffi_last_error_message(), None),
700 }
701 }
702}
703
704pub(crate) fn check_status_in(status: cudf_sys::cudf_status_t, operation: &'static str) -> Result<()> {
707 if status == cudf_sys::CUDF_SUCCESS {
708 return Ok(());
709 }
710 let (message, ffi_detail) = read_ffi_error(status);
713 if status == cudf_sys::CUDF_ERROR_FATAL_CUDA
714 && let Some(device) = runtime::device_guard::current_entered_device()
715 {
716 runtime::report_device_fault(
717 device,
718 runtime::DeviceFaultKind::FatalDeviceOperation,
719 operation,
720 Some(runtime::DeviceFaultNativeStatus::Cudf(status)),
721 );
722 }
723 Err(error_for_ffi_status_with_detail(status, operation, message, ffi_detail))
724}
725
726pub(crate) fn check_status_in_without_device_report(
733 status: cudf_sys::cudf_status_t,
734 operation: &'static str,
735) -> Result<()> {
736 if status == cudf_sys::CUDF_SUCCESS {
737 return Ok(());
738 }
739 let (message, ffi_detail) = read_ffi_error(status);
740 Err(error_for_ffi_status_with_detail(status, operation, message, ffi_detail))
741}
742
743pub(crate) fn check_status_in_with_details<I>(
751 status: cudf_sys::cudf_status_t,
752 operation: &'static str,
753 details: I,
754) -> Result<()>
755where
756 I: IntoIterator<Item = (&'static str, String)>,
757{
758 check_status_in(status, operation).map_err(|err| err.with_details(details))
759}
760
761fn leaf_code_for_interop_kind(kind: InteropErrorKind) -> ErrorCode {
762 match kind {
763 InteropErrorKind::NotFound => ErrorCode::ForeignInvalidArgument,
764 InteropErrorKind::UnsupportedType => ErrorCode::UnsupportedDtype,
765 InteropErrorKind::InvalidView => ErrorCode::ForeignInvalidArgument,
766 InteropErrorKind::ExecutionBindingUnavailable => ErrorCode::ExecutionBindingUnavailable,
767 InteropErrorKind::ReadinessUnavailable => ErrorCode::ReadinessUnavailable,
768 InteropErrorKind::UnsupportedLayout => ErrorCode::UnsupportedLayout,
769 InteropErrorKind::PlacementUnsupported | InteropErrorKind::ProfileUnsupported => {
770 ErrorCode::UnsupportedOperation
771 },
772 InteropErrorKind::MixedDeviceUnsupported => ErrorCode::CrossDeviceInputs,
773 InteropErrorKind::ExecutionBindingMismatch | InteropErrorKind::ReadinessMismatch => {
774 ErrorCode::RuntimeBindingMismatch
775 },
776 }
777}
778
779pub(crate) fn from_interop_in(error: InteropError, operation: &'static str) -> Error {
794 let code = leaf_code_for_interop_kind(error.kind());
795 let message = error.message().to_string();
796
797 let mut out = Error::new_with(code, operation, message);
798
799 for (key, value) in error.context() {
800 match *key {
801 "operation" => {
802 out = out.with_detail("foreign_operation", value.clone());
803 },
804 "called" => {
805 out = out.with_detail("foreign_called", value.clone());
806 },
807 other => {
808 out = attach_foreign_context_entry(out, other, value);
809 },
810 }
811 }
812
813 out.with_source(error)
814}
815
816fn attach_foreign_context_entry(err: Error, key: &'static str, value: &str) -> Error {
830 match foreign_device_role(key) {
831 Some(ForeignDeviceRole::Left) => {
832 attach_foreign_device_fact(err, FACT_LEFT_DEVICE, "foreign_left_device_raw", value)
833 },
834 Some(ForeignDeviceRole::Right) => {
835 attach_foreign_device_fact(err, FACT_RIGHT_DEVICE, "foreign_right_device_raw", value)
836 },
837 None => match foreign_context_detail_key(key) {
838 Some(rewritten) => err.with_detail(rewritten, value.to_string()),
839 None => err.with_detail(key, value.to_string()),
840 },
841 }
842}
843
844fn attach_foreign_device_fact(
845 err: Error,
846 fact_key: FactKey<DeviceIdFact>,
847 fallback_detail_key: &'static str,
848 value: &str,
849) -> Error {
850 match value.parse::<i32>() {
851 Ok(device_id) if err.fact_value(fact_key.name()).is_none() => err.with_fact(fact_key, DeviceIdFact(device_id)),
852 _ => err.with_detail(fallback_detail_key, value.to_string()),
853 }
854}
855
856#[derive(Debug, Clone, Copy, PartialEq, Eq)]
857enum ForeignDeviceRole {
858 Left,
859 Right,
860}
861
862fn foreign_device_role(key: &str) -> Option<ForeignDeviceRole> {
865 match key {
866 "producer_device" | "expected_device" | "descriptor_device" | "release_device" => Some(ForeignDeviceRole::Left),
867 "runtime_device" | "actual_device" | "event_device" | "stream_device" | "consumer_device" => {
868 Some(ForeignDeviceRole::Right)
869 },
870 _ => None,
871 }
872}
873
874fn foreign_context_detail_key(key: &'static str) -> Option<&'static str> {
878 FactKeyName::from_snake_case(key)?;
879 Some(match key {
880 "column_name" => "foreign_column_name",
881 "column_index" => "foreign_column_index",
882 "expected_rows" => "foreign_expected_rows",
883 "actual_rows" => "foreign_actual_rows",
884 "expected_count" => "foreign_expected_count",
885 "actual_count" => "foreign_actual_count",
886 "left_type_id" => "foreign_left_type_id",
887 "right_type_id" => "foreign_right_type_id",
888 "left_device" => "foreign_left_device_raw",
889 "right_device" => "foreign_right_device_raw",
890 "table_row_count" => "foreign_table_row_count",
891 "table_column_count" => "foreign_table_column_count",
892 "keys_count" => "foreign_keys_count",
893 "distinct_keys_count" => "foreign_distinct_keys_count",
894 "type_id" => "foreign_type_id",
895 "execution_binding_id" => "foreign_execution_binding_id",
896 "validation_reason" => "foreign_validation_reason",
897 "validation_device" => "foreign_validation_device",
898 "validation_stream" => "foreign_validation_stream",
899 "control_plane_scalar_bytes" => "foreign_control_plane_scalar_bytes",
900 _ => "foreign_canonical_context",
903 })
904}
905
906pub fn find_cudf_error_in_chain<'a>(error: &'a (dyn StdError + 'static)) -> Option<&'a Error> {
908 let mut current = Some(error);
909 while let Some(err) = current {
910 if let Some(cudf_error) = err.downcast_ref::<Error>() {
911 return Some(cudf_error);
912 }
913 current = err.source();
914 }
915 None
916}
917
918#[cfg(test)]
919mod size_overflow_tests {
920 use super::*;
921
922 #[test]
923 fn native_size_overflow_status_maps_to_permanent_resource_error() {
924 let error = error_for_ffi_status(
925 cudf_sys::CUDF_ERROR_SIZE_OVERFLOW,
926 "cudf::test.size_overflow",
927 "synthetic size overflow".to_string(),
928 );
929
930 assert_eq!(error.code(), ErrorCode::SizeOverflow);
931 assert_eq!(error.kind(), ErrorKind::ResourceExhausted);
932 assert_eq!(error.status(), ErrorStatus::Permanent);
933 assert!(!error.is_retryable());
934 }
935}
936
937#[cfg(test)]
938mod ffi_detail_tests {
939 use super::*;
940
941 #[test]
942 fn ffi_detail_is_retained_as_typed_error_payload() {
943 let detail = CudfErrorDetail {
944 status: cudf_sys::CUDF_ERROR_OUT_OF_MEMORY,
945 failure_source: CudfErrorFailureSource::ReservationLimit,
946 domain_id: 41,
947 member_id: 3,
948 generation: 9,
949 requested_bytes: 4096,
950 alignment: 256,
951 cuda_error: -1,
952 message: "query memory domain grant exceeded".to_string(),
953 };
954 let error = error_for_ffi_status_with_detail(
955 cudf_sys::CUDF_ERROR_OUT_OF_MEMORY,
956 "testing::ffi_detail",
957 detail.message.clone(),
958 Some(detail.clone()),
959 );
960
961 assert_eq!(error.code(), ErrorCode::OutOfMemory);
962 assert_eq!(error.ffi_detail(), Some(&detail));
963 assert!(error.diagnostic_string().contains("failure_source=RESERVATION_LIMIT"));
964 }
965}