1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5#[derive(Debug, Error, Clone, Serialize, Deserialize)]
11#[non_exhaustive]
12pub enum DataflowError {
13 #[error("Validation error: {0}")]
15 Validation(String),
16
17 #[error("Function execution error: {context}")]
19 FunctionExecution {
20 context: String,
21 #[source]
22 #[serde(skip)]
23 source: Option<Box<DataflowError>>,
24 },
25
26 #[error("Workflow error: {0}")]
28 Workflow(String),
29
30 #[error("Task error: {0}")]
32 Task(String),
33
34 #[error("Function not found: {0}")]
36 FunctionNotFound(String),
37
38 #[error("Deserialization error: {0}")]
40 Deserialization(String),
41
42 #[error("IO error: {0}")]
44 Io(String),
45
46 #[error("Logic evaluation error: {0}")]
48 LogicEvaluation(String),
49
50 #[error("HTTP error: {status} - {message}")]
52 Http { status: u16, message: String },
53
54 #[error("Timeout error: {0}")]
56 Timeout(String),
57
58 #[error("Unknown error: {0}")]
60 Unknown(String),
61
62 #[error("{message}")]
75 Service {
76 kind: String,
78 message: String,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
83 detail: Option<String>,
84 retryable: bool,
87 },
88}
89
90impl DataflowError {
91 pub fn function_execution<S: Into<String>>(context: S, source: Option<DataflowError>) -> Self {
93 DataflowError::FunctionExecution {
94 context: context.into(),
95 source: source.map(Box::new),
96 }
97 }
98
99 pub fn http<S: Into<String>>(status: u16, message: S) -> Self {
101 DataflowError::Http {
102 status,
103 message: message.into(),
104 }
105 }
106
107 pub fn from_io(err: std::io::Error) -> Self {
109 DataflowError::Io(err.to_string())
110 }
111
112 pub fn from_serde(err: serde_json::Error) -> Self {
114 DataflowError::Deserialization(err.to_string())
115 }
116
117 pub fn retryable(&self) -> bool {
123 match self {
124 DataflowError::Http { status, .. } => {
126 *status >= 500 || *status == 429 || *status == 408 || *status == 0
128 }
130 DataflowError::Timeout(_) => true,
131 DataflowError::Io(_) => true,
132 DataflowError::FunctionExecution { source, .. } => {
133 source.as_ref().map(|e| e.retryable()).unwrap_or(false)
135 }
136
137 DataflowError::Validation(_) => false,
139 DataflowError::LogicEvaluation(_) => false,
140 DataflowError::Deserialization(_) => false,
141 DataflowError::Workflow(_) => false,
142 DataflowError::Task(_) => false,
143 DataflowError::FunctionNotFound(_) => false,
144 DataflowError::Unknown(_) => false,
145
146 DataflowError::Service { retryable, .. } => *retryable,
148 }
149 }
150
151 pub fn kind(&self) -> Option<&str> {
157 match self {
158 DataflowError::Service { kind, .. } => Some(kind),
159 DataflowError::FunctionExecution { source, .. } => {
162 source.as_deref().and_then(DataflowError::kind)
163 }
164 _ => None,
165 }
166 }
167
168 pub fn detail(&self) -> Option<&str> {
174 match self {
175 DataflowError::Service { detail, .. } => detail.as_deref(),
176 DataflowError::FunctionExecution { source, .. } => {
177 source.as_deref().and_then(DataflowError::detail)
178 }
179 _ => None,
180 }
181 }
182
183 pub fn service(kind: impl Into<String>, message: impl Into<String>) -> ServiceErrorBuilder {
203 ServiceErrorBuilder {
204 kind: kind.into(),
205 message: message.into(),
206 detail: None,
207 retryable: false,
208 }
209 }
210}
211
212#[must_use = "ServiceErrorBuilder must be `.build()` to produce a DataflowError"]
214pub struct ServiceErrorBuilder {
215 kind: String,
216 message: String,
217 detail: Option<String>,
218 retryable: bool,
219}
220
221impl ServiceErrorBuilder {
222 pub fn detail(mut self, detail: impl Into<String>) -> Self {
225 self.detail = Some(detail.into());
226 self
227 }
228
229 pub fn retryable(mut self, retryable: bool) -> Self {
233 self.retryable = retryable;
234 self
235 }
236
237 pub fn build(self) -> DataflowError {
238 DataflowError::Service {
239 kind: self.kind,
240 message: self.message,
241 detail: self.detail,
242 retryable: self.retryable,
243 }
244 }
245}
246
247pub type Result<T> = std::result::Result<T, DataflowError>;
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
257#[non_exhaustive]
258pub struct ErrorInfo {
259 pub code: String,
261
262 pub message: String,
264
265 pub path: Option<String>,
267
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub workflow_id: Option<String>,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub task_id: Option<String>,
275
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub timestamp: Option<String>,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub retry_attempted: Option<bool>,
283
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub retry_count: Option<u32>,
287
288 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub detail: Option<String>,
296}
297
298pub(crate) fn service_error_code(error: &DataflowError) -> String {
309 match error.kind() {
310 Some(kind) if !kind.is_empty() => kind.to_string(),
311 _ => "TASK_ERROR".to_string(),
312 }
313}
314
315impl ErrorInfo {
316 pub fn new(workflow_id: Option<String>, task_id: Option<String>, error: DataflowError) -> Self {
318 Self {
319 code: match &error {
320 DataflowError::Validation(_) => "VALIDATION_ERROR".to_string(),
321 DataflowError::Workflow(_) => "WORKFLOW_ERROR".to_string(),
322 DataflowError::Task(_) => "TASK_ERROR".to_string(),
323 DataflowError::FunctionNotFound(_) => "FUNCTION_NOT_FOUND".to_string(),
324 DataflowError::FunctionExecution { .. } => "FUNCTION_ERROR".to_string(),
325 DataflowError::LogicEvaluation(_) => "LOGIC_ERROR".to_string(),
326 DataflowError::Http { .. } => "HTTP_ERROR".to_string(),
327 DataflowError::Timeout(_) => "TIMEOUT_ERROR".to_string(),
328 DataflowError::Io(_) => "IO_ERROR".to_string(),
329 DataflowError::Deserialization(_) => "DESERIALIZATION_ERROR".to_string(),
330 DataflowError::Unknown(_) => "UNKNOWN_ERROR".to_string(),
331 DataflowError::Service { kind, .. } => {
336 if kind.is_empty() {
337 "TASK_ERROR".to_string()
338 } else {
339 kind.clone()
340 }
341 }
342 },
343 detail: error.detail().map(str::to_string),
344 message: error.to_string(),
345 path: None,
346 workflow_id,
347 task_id,
348 timestamp: Some(Utc::now().to_rfc3339()),
349 retry_attempted: Some(false),
350 retry_count: Some(0),
351 }
352 }
353
354 pub fn simple(code: String, message: String, path: Option<String>) -> Self {
356 Self {
357 code,
358 message,
359 path,
360 workflow_id: None,
361 task_id: None,
362 timestamp: Some(Utc::now().to_rfc3339()),
363 retry_attempted: None,
364 retry_count: None,
365 detail: None,
366 }
367 }
368
369 pub fn simple_ref(code: &str, message: &str, path: Option<&str>) -> Self {
371 Self {
372 code: code.to_string(),
373 message: message.to_string(),
374 path: path.map(|s| s.to_string()),
375 workflow_id: None,
376 task_id: None,
377 timestamp: Some(Utc::now().to_rfc3339()),
378 retry_attempted: None,
379 retry_count: None,
380 detail: None,
381 }
382 }
383
384 pub fn with_retry(mut self) -> Self {
386 self.retry_attempted = Some(true);
387 self.retry_count = Some(self.retry_count.unwrap_or(0) + 1);
388 self
389 }
390
391 pub fn builder(code: impl Into<String>, message: impl Into<String>) -> ErrorInfoBuilder {
393 ErrorInfoBuilder::new(code, message)
394 }
395}
396
397#[must_use = "ErrorInfoBuilder must be `.build()` to produce an ErrorInfo"]
399pub struct ErrorInfoBuilder {
400 code: String,
401 message: String,
402 path: Option<String>,
403 workflow_id: Option<String>,
404 task_id: Option<String>,
405 timestamp: Option<String>,
406 retry_attempted: Option<bool>,
407 retry_count: Option<u32>,
408 detail: Option<String>,
409}
410
411impl ErrorInfoBuilder {
412 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
414 Self {
415 code: code.into(),
416 message: message.into(),
417 path: None,
418 workflow_id: None,
419 task_id: None,
420 timestamp: Some(Utc::now().to_rfc3339()),
421 retry_attempted: None,
422 retry_count: None,
423 detail: None,
424 }
425 }
426
427 pub fn path(mut self, path: impl Into<String>) -> Self {
429 self.path = Some(path.into());
430 self
431 }
432
433 pub fn workflow_id(mut self, id: impl Into<String>) -> Self {
435 self.workflow_id = Some(id.into());
436 self
437 }
438
439 pub fn task_id(mut self, id: impl Into<String>) -> Self {
441 self.task_id = Some(id.into());
442 self
443 }
444
445 pub fn timestamp(mut self, timestamp: impl Into<String>) -> Self {
447 self.timestamp = Some(timestamp.into());
448 self
449 }
450
451 pub fn retry_attempted(mut self, attempted: bool) -> Self {
453 self.retry_attempted = Some(attempted);
454 self
455 }
456
457 pub fn retry_count(mut self, count: u32) -> Self {
459 self.retry_count = Some(count);
460 self
461 }
462
463 pub fn detail(mut self, detail: impl Into<String>) -> Self {
466 self.detail = Some(detail.into());
467 self
468 }
469
470 pub fn build(self) -> ErrorInfo {
472 ErrorInfo {
473 code: self.code,
474 message: self.message,
475 path: self.path,
476 workflow_id: self.workflow_id,
477 task_id: self.task_id,
478 timestamp: self.timestamp,
479 retry_attempted: self.retry_attempted,
480 retry_count: self.retry_count,
481 detail: self.detail,
482 }
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn test_retryable_errors() {
492 assert!(
494 DataflowError::Http {
495 status: 500,
496 message: "Internal Server Error".to_string()
497 }
498 .retryable()
499 );
500 assert!(
501 DataflowError::Http {
502 status: 502,
503 message: "Bad Gateway".to_string()
504 }
505 .retryable()
506 );
507 assert!(
508 DataflowError::Http {
509 status: 503,
510 message: "Service Unavailable".to_string()
511 }
512 .retryable()
513 );
514 assert!(
515 DataflowError::Http {
516 status: 429,
517 message: "Too Many Requests".to_string()
518 }
519 .retryable()
520 );
521 assert!(
522 DataflowError::Http {
523 status: 408,
524 message: "Request Timeout".to_string()
525 }
526 .retryable()
527 );
528 assert!(
529 DataflowError::Http {
530 status: 0,
531 message: "Connection Error".to_string()
532 }
533 .retryable()
534 );
535 assert!(DataflowError::Timeout("Connection timeout".to_string()).retryable());
536 assert!(DataflowError::Io("Network error".to_string()).retryable());
537 }
538
539 #[test]
540 fn test_non_retryable_errors() {
541 assert!(
543 !DataflowError::Http {
544 status: 400,
545 message: "Bad Request".to_string()
546 }
547 .retryable()
548 );
549 assert!(
550 !DataflowError::Http {
551 status: 401,
552 message: "Unauthorized".to_string()
553 }
554 .retryable()
555 );
556 assert!(
557 !DataflowError::Http {
558 status: 403,
559 message: "Forbidden".to_string()
560 }
561 .retryable()
562 );
563 assert!(
564 !DataflowError::Http {
565 status: 404,
566 message: "Not Found".to_string()
567 }
568 .retryable()
569 );
570 assert!(!DataflowError::Validation("Invalid input".to_string()).retryable());
571 assert!(!DataflowError::LogicEvaluation("Invalid logic".to_string()).retryable());
572 assert!(!DataflowError::Deserialization("Invalid JSON".to_string()).retryable());
573 assert!(!DataflowError::Workflow("Invalid workflow".to_string()).retryable());
574 assert!(!DataflowError::Unknown("Unknown error".to_string()).retryable());
575 }
576
577 #[test]
578 fn test_function_execution_error_retryability() {
579 let retryable_source = DataflowError::Http {
581 status: 500,
582 message: "Server Error".to_string(),
583 };
584 let non_retryable_source = DataflowError::Validation("Invalid data".to_string());
585
586 let retryable_func_error =
587 DataflowError::function_execution("HTTP call failed", Some(retryable_source));
588 let non_retryable_func_error =
589 DataflowError::function_execution("Validation failed", Some(non_retryable_source));
590 let no_source_func_error = DataflowError::function_execution("Unknown failure", None);
591
592 assert!(retryable_func_error.retryable());
593 assert!(!non_retryable_func_error.retryable());
594 assert!(!no_source_func_error.retryable());
595 }
596
597 #[test]
598 fn test_error_info_builder() {
599 let error = ErrorInfo::builder("TEST_ERROR", "Test message").build();
601 assert_eq!(error.code, "TEST_ERROR");
602 assert_eq!(error.message, "Test message");
603 assert!(error.timestamp.is_some());
604 assert!(error.path.is_none());
605
606 let error = ErrorInfo::builder("VALIDATION_ERROR", "Field validation failed")
608 .path("data.email")
609 .workflow_id("workflow_1")
610 .task_id("validate_email")
611 .retry_attempted(true)
612 .retry_count(2)
613 .build();
614
615 assert_eq!(error.code, "VALIDATION_ERROR");
616 assert_eq!(error.message, "Field validation failed");
617 assert_eq!(error.path, Some("data.email".to_string()));
618 assert_eq!(error.workflow_id, Some("workflow_1".to_string()));
619 assert_eq!(error.task_id, Some("validate_email".to_string()));
620 assert_eq!(error.retry_attempted, Some(true));
621 assert_eq!(error.retry_count, Some(2));
622 }
623
624 #[test]
625 fn test_error_info_new_from_dataflow_error() {
626 let test_cases = vec![
628 (
629 DataflowError::Validation("test".to_string()),
630 "VALIDATION_ERROR",
631 ),
632 (
633 DataflowError::Workflow("test".to_string()),
634 "WORKFLOW_ERROR",
635 ),
636 (DataflowError::Task("test".to_string()), "TASK_ERROR"),
637 (
638 DataflowError::FunctionNotFound("test".to_string()),
639 "FUNCTION_NOT_FOUND",
640 ),
641 (
642 DataflowError::function_execution("test", None),
643 "FUNCTION_ERROR",
644 ),
645 (
646 DataflowError::LogicEvaluation("test".to_string()),
647 "LOGIC_ERROR",
648 ),
649 (DataflowError::http(404, "Not Found"), "HTTP_ERROR"),
650 (DataflowError::Timeout("test".to_string()), "TIMEOUT_ERROR"),
651 (DataflowError::Io("test".to_string()), "IO_ERROR"),
652 (
653 DataflowError::Deserialization("test".to_string()),
654 "DESERIALIZATION_ERROR",
655 ),
656 (DataflowError::Unknown("test".to_string()), "UNKNOWN_ERROR"),
657 ];
658
659 for (error, expected_code) in test_cases {
660 let info = ErrorInfo::new(
661 Some("workflow_1".to_string()),
662 Some("task_1".to_string()),
663 error,
664 );
665 assert_eq!(info.code, expected_code);
666 assert_eq!(info.workflow_id, Some("workflow_1".to_string()));
667 assert_eq!(info.task_id, Some("task_1".to_string()));
668 assert!(info.timestamp.is_some());
669 assert_eq!(info.retry_attempted, Some(false));
670 assert_eq!(info.retry_count, Some(0));
671 }
672 }
673
674 #[test]
675 fn test_error_info_simple_constructors() {
676 let error = ErrorInfo::simple(
678 "CUSTOM_ERROR".to_string(),
679 "Custom message".to_string(),
680 Some("data.field".to_string()),
681 );
682 assert_eq!(error.code, "CUSTOM_ERROR");
683 assert_eq!(error.message, "Custom message");
684 assert_eq!(error.path, Some("data.field".to_string()));
685 assert!(error.workflow_id.is_none());
686 assert!(error.task_id.is_none());
687 assert!(error.timestamp.is_some());
688
689 let error = ErrorInfo::simple_ref("REF_ERROR", "Ref message", Some("data.path"));
691 assert_eq!(error.code, "REF_ERROR");
692 assert_eq!(error.message, "Ref message");
693 assert_eq!(error.path, Some("data.path".to_string()));
694
695 let error = ErrorInfo::simple_ref("NO_PATH", "No path message", None);
697 assert!(error.path.is_none());
698 }
699
700 #[test]
701 fn test_error_info_with_retry() {
702 let error = ErrorInfo::simple_ref("TEST", "Test", None);
703 assert!(error.retry_attempted.is_none());
704 assert!(error.retry_count.is_none());
705
706 let error = error.with_retry();
707 assert_eq!(error.retry_attempted, Some(true));
708 assert_eq!(error.retry_count, Some(1));
709
710 let error = error.with_retry();
711 assert_eq!(error.retry_attempted, Some(true));
712 assert_eq!(error.retry_count, Some(2));
713 }
714
715 #[test]
716 fn test_error_display_messages() {
717 assert_eq!(
719 DataflowError::Validation("test".to_string()).to_string(),
720 "Validation error: test"
721 );
722 assert_eq!(
723 DataflowError::Workflow("test".to_string()).to_string(),
724 "Workflow error: test"
725 );
726 assert_eq!(
727 DataflowError::Task("test".to_string()).to_string(),
728 "Task error: test"
729 );
730 assert_eq!(
731 DataflowError::FunctionNotFound("test".to_string()).to_string(),
732 "Function not found: test"
733 );
734 assert_eq!(
735 DataflowError::http(404, "Not Found").to_string(),
736 "HTTP error: 404 - Not Found"
737 );
738 assert_eq!(
739 DataflowError::Timeout("test".to_string()).to_string(),
740 "Timeout error: test"
741 );
742 }
743
744 #[test]
745 fn test_error_conversions() {
746 let json_str = "invalid json";
748 let serde_result: std::result::Result<serde_json::Value, _> =
749 serde_json::from_str(json_str);
750 if let Err(e) = serde_result {
751 let dataflow_err = DataflowError::from_serde(e);
752 assert!(matches!(dataflow_err, DataflowError::Deserialization(_)));
753 }
754 }
755
756 #[test]
757 fn service_error_carries_kind_detail_and_declared_retryability() {
758 let e = DataflowError::service("circuit_open", "upstream unavailable")
759 .detail("connector 'billing' breaker open")
760 .retryable(true)
761 .build();
762
763 assert_eq!(e.kind(), Some("circuit_open"));
764 assert_eq!(e.detail(), Some("connector 'billing' breaker open"));
765 assert!(e.retryable());
766 }
767
768 #[test]
769 fn function_execution_inherits_kind_detail_and_retryable_from_a_wrapped_service_source() {
770 let inner = DataflowError::service("circuit_open", "upstream unavailable")
776 .detail("connector 'billing' breaker open since 12:04")
777 .retryable(true)
778 .build();
779 let wrapped = DataflowError::function_execution("calling billing connector", Some(inner));
780
781 assert_eq!(wrapped.kind(), Some("circuit_open"));
782 assert_eq!(
783 wrapped.detail(),
784 Some("connector 'billing' breaker open since 12:04")
785 );
786 assert!(wrapped.retryable());
787
788 assert_eq!(service_error_code(&wrapped), "circuit_open");
791 }
792
793 #[test]
794 fn service_display_hides_the_detail_but_debug_shows_it() {
795 let e = DataflowError::service("circuit_open", "upstream unavailable")
796 .detail("SECRET-TOPOLOGY")
797 .build();
798
799 assert_eq!(e.to_string(), "upstream unavailable");
801 assert!(!e.to_string().contains("SECRET-TOPOLOGY"));
802 assert!(format!("{e:?}").contains("SECRET-TOPOLOGY"));
804 }
805
806 #[test]
807 fn service_retryability_is_independent_of_every_other_field() {
808 let yes = DataflowError::service("k", "m").retryable(true).build();
809 let no = DataflowError::service("k", "m").retryable(false).build();
810 assert!(yes.retryable());
811 assert!(!no.retryable());
812 assert!(!DataflowError::service("k", "m").build().retryable());
814 }
815
816 #[test]
817 fn kind_and_detail_are_none_for_every_engine_owned_variant() {
818 let variants = [
819 DataflowError::Validation("v".into()),
820 DataflowError::FunctionExecution {
821 context: "c".into(),
822 source: None,
823 },
824 DataflowError::LogicEvaluation("l".into()),
825 DataflowError::Deserialization("d".into()),
826 DataflowError::Workflow("w".into()),
827 DataflowError::Task("t".into()),
828 DataflowError::FunctionNotFound("f".into()),
829 DataflowError::Http {
830 status: 500,
831 message: "h".into(),
832 },
833 DataflowError::Timeout("to".into()),
834 DataflowError::Io("io".into()),
835 DataflowError::Unknown("u".into()),
836 ];
837 assert_eq!(variants.len(), 11, "one assertion per engine-owned variant");
838 for v in &variants {
839 assert_eq!(v.kind(), None, "kind() for {v:?}");
840 assert_eq!(v.detail(), None, "detail() for {v:?}");
841 }
842 }
843
844 #[test]
845 fn service_error_code_passes_kind_through_verbatim() {
846 let e = DataflowError::service("circuit_open", "m").build();
849 let info = ErrorInfo::new(None, None, e);
850 assert_eq!(info.code, "circuit_open");
851 }
852
853 #[test]
854 fn an_empty_kind_falls_back_rather_than_recording_an_empty_code() {
855 let e = DataflowError::service("", "m").build();
856 let info = ErrorInfo::new(None, None, e);
857 assert_eq!(info.code, "TASK_ERROR");
858 assert!(!info.code.is_empty());
859 }
860
861 #[test]
862 fn a_non_ascii_kind_is_neither_panicked_on_nor_mangled() {
863 let e = DataflowError::service("limite_dépassé", "m").build();
864 let info = ErrorInfo::new(None, None, e);
865 assert_eq!(info.code, "limite_dépassé");
867 }
868
869 #[test]
870 fn error_info_lifts_the_detail_and_omits_it_when_absent() {
871 let with = ErrorInfo::new(
872 None,
873 None,
874 DataflowError::service("k", "m").detail("op only").build(),
875 );
876 assert_eq!(with.detail.as_deref(), Some("op only"));
877 assert!(serde_json::to_string(&with).unwrap().contains("detail"));
878
879 let without = ErrorInfo::new(None, None, DataflowError::service("k", "m").build());
881 assert_eq!(without.detail, None);
882 assert!(!serde_json::to_string(&without).unwrap().contains("detail"));
883
884 let engine_owned = ErrorInfo::new(None, None, DataflowError::Task("t".into()));
885 assert_eq!(engine_owned.detail, None);
886 assert!(
887 !serde_json::to_string(&engine_owned)
888 .unwrap()
889 .contains("detail"),
890 "the JSON shape is unchanged for every pre-existing error"
891 );
892 }
893
894 #[test]
895 fn a_service_error_round_trips_through_serde_with_and_without_detail() {
896 for e in [
897 DataflowError::service("k", "m")
898 .detail("d")
899 .retryable(true)
900 .build(),
901 DataflowError::service("k", "m").build(),
902 ] {
903 let json = serde_json::to_string(&e).unwrap();
904 let back: DataflowError = serde_json::from_str(&json).unwrap();
905 assert_eq!(back.kind(), e.kind());
906 assert_eq!(back.detail(), e.detail());
907 assert_eq!(back.retryable(), e.retryable());
908 }
909
910 let bare = DataflowError::service("k", "m").build();
912 assert!(!serde_json::to_string(&bare).unwrap().contains("detail"));
913 }
914}