1use serde::{Deserialize, Serialize};
18use thiserror::Error;
19
20pub type DurableResult<T> = Result<T, DurableError>;
49
50pub type StepResult<T> = Result<T, DurableError>;
76
77pub type CheckpointResult<T> = Result<T, DurableError>;
103
104#[derive(Debug, Error)]
141pub enum DurableError {
142 #[error("Execution error: {message}")]
144 Execution {
145 message: String,
147 termination_reason: TerminationReason,
149 },
150
151 #[error("Invocation error: {message}")]
153 Invocation {
154 message: String,
156 termination_reason: TerminationReason,
158 },
159
160 #[error("Checkpoint error: {message}")]
162 Checkpoint {
163 message: String,
165 is_retriable: bool,
167 aws_error: Option<AwsError>,
169 },
170
171 #[error("Callback error: {message}")]
173 Callback {
174 message: String,
176 callback_id: Option<String>,
178 },
179
180 #[error("Non-deterministic execution: {message}")]
182 NonDeterministic {
183 message: String,
185 operation_id: Option<String>,
187 },
188
189 #[error("Validation error: {message}")]
191 Validation {
192 message: String,
194 },
195
196 #[error("Serialization error: {message}")]
198 SerDes {
199 message: String,
201 },
202
203 #[error("Suspend execution")]
205 Suspend {
206 scheduled_timestamp: Option<f64>,
208 },
209
210 #[error("Orphaned child: {message}")]
212 OrphanedChild {
213 message: String,
215 operation_id: String,
217 },
218
219 #[error("User code error: {message}")]
221 UserCode {
222 message: String,
224 error_type: String,
226 stack_trace: Option<String>,
228 },
229
230 #[error("Size limit exceeded: {message}")]
239 SizeLimit {
240 message: String,
242 actual_size: Option<usize>,
244 max_size: Option<usize>,
246 },
247
248 #[error("Throttling: {message}")]
253 Throttling {
254 message: String,
256 retry_after_ms: Option<u64>,
258 },
259
260 #[error("Resource not found: {message}")]
269 ResourceNotFound {
270 message: String,
272 resource_id: Option<String>,
274 },
275
276 #[error("Configuration error: {message}")]
285 Configuration {
286 message: String,
288 },
289}
290
291impl DurableError {
292 pub fn execution(message: impl Into<String>) -> Self {
294 Self::Execution {
295 message: message.into(),
296 termination_reason: TerminationReason::ExecutionError,
297 }
298 }
299
300 pub fn invocation(message: impl Into<String>) -> Self {
302 Self::Invocation {
303 message: message.into(),
304 termination_reason: TerminationReason::InvocationError,
305 }
306 }
307
308 pub fn checkpoint_retriable(message: impl Into<String>) -> Self {
310 Self::Checkpoint {
311 message: message.into(),
312 is_retriable: true,
313 aws_error: None,
314 }
315 }
316
317 pub fn checkpoint_non_retriable(message: impl Into<String>) -> Self {
319 Self::Checkpoint {
320 message: message.into(),
321 is_retriable: false,
322 aws_error: None,
323 }
324 }
325
326 pub fn validation(message: impl Into<String>) -> Self {
328 Self::Validation {
329 message: message.into(),
330 }
331 }
332
333 pub fn serdes(message: impl Into<String>) -> Self {
335 Self::SerDes {
336 message: message.into(),
337 }
338 }
339
340 pub fn suspend() -> Self {
342 Self::Suspend {
343 scheduled_timestamp: None,
344 }
345 }
346
347 pub fn suspend_until(timestamp: f64) -> Self {
349 Self::Suspend {
350 scheduled_timestamp: Some(timestamp),
351 }
352 }
353
354 pub fn size_limit(message: impl Into<String>) -> Self {
360 Self::SizeLimit {
361 message: message.into(),
362 actual_size: None,
363 max_size: None,
364 }
365 }
366
367 pub fn size_limit_with_details(
375 message: impl Into<String>,
376 actual_size: usize,
377 max_size: usize,
378 ) -> Self {
379 Self::SizeLimit {
380 message: message.into(),
381 actual_size: Some(actual_size),
382 max_size: Some(max_size),
383 }
384 }
385
386 pub fn throttling(message: impl Into<String>) -> Self {
392 Self::Throttling {
393 message: message.into(),
394 retry_after_ms: None,
395 }
396 }
397
398 pub fn throttling_with_retry_delay(message: impl Into<String>, retry_after_ms: u64) -> Self {
405 Self::Throttling {
406 message: message.into(),
407 retry_after_ms: Some(retry_after_ms),
408 }
409 }
410
411 pub fn resource_not_found(message: impl Into<String>) -> Self {
417 Self::ResourceNotFound {
418 message: message.into(),
419 resource_id: None,
420 }
421 }
422
423 pub fn resource_not_found_with_id(
430 message: impl Into<String>,
431 resource_id: impl Into<String>,
432 ) -> Self {
433 Self::ResourceNotFound {
434 message: message.into(),
435 resource_id: Some(resource_id.into()),
436 }
437 }
438
439 pub fn is_retriable(&self) -> bool {
441 matches!(
442 self,
443 Self::Checkpoint {
444 is_retriable: true,
445 ..
446 }
447 )
448 }
449
450 pub fn is_suspend(&self) -> bool {
452 matches!(self, Self::Suspend { .. })
453 }
454
455 pub fn is_invalid_checkpoint_token(&self) -> bool {
464 match self {
465 Self::Checkpoint {
466 aws_error: Some(aws_error),
467 ..
468 } => {
469 aws_error.code == "InvalidParameterValueException"
470 && aws_error.message.contains("Invalid checkpoint token")
471 }
472 Self::Checkpoint { message, .. } => message.contains("Invalid checkpoint token"),
473 _ => false,
474 }
475 }
476
477 pub fn is_size_limit(&self) -> bool {
481 matches!(self, Self::SizeLimit { .. })
482 }
483
484 pub fn is_throttling(&self) -> bool {
488 matches!(self, Self::Throttling { .. })
489 }
490
491 pub fn is_resource_not_found(&self) -> bool {
495 matches!(self, Self::ResourceNotFound { .. })
496 }
497
498 pub fn get_retry_after_ms(&self) -> Option<u64> {
502 match self {
503 Self::Throttling { retry_after_ms, .. } => *retry_after_ms,
504 _ => None,
505 }
506 }
507}
508
509#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
514#[repr(u8)]
515pub enum TerminationReason {
516 #[default]
518 UnhandledError = 0,
519 InvocationError = 1,
521 ExecutionError = 2,
523 CheckpointFailed = 3,
525 NonDeterministicExecution = 4,
527 StepInterrupted = 5,
529 CallbackError = 6,
531 SerializationError = 7,
533 SizeLimitExceeded = 8,
535 OperationTerminated = 9,
537 RetryScheduled = 10,
539 WaitScheduled = 11,
541 CallbackPending = 12,
543 ContextValidationError = 13,
545 LambdaTimeoutApproaching = 14,
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct AwsError {
552 pub code: String,
554 pub message: String,
556 pub request_id: Option<String>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct ErrorObject {
594 #[serde(rename = "ErrorType")]
596 pub error_type: String,
597 #[serde(rename = "ErrorMessage")]
599 pub error_message: String,
600 #[serde(rename = "StackTrace", skip_serializing_if = "Option::is_none")]
602 pub stack_trace: Option<String>,
603}
604
605impl ErrorObject {
606 pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
608 Self {
609 error_type: error_type.into(),
610 error_message: error_message.into(),
611 stack_trace: None,
612 }
613 }
614
615 pub fn with_stack_trace(
617 error_type: impl Into<String>,
618 error_message: impl Into<String>,
619 stack_trace: impl Into<String>,
620 ) -> Self {
621 Self {
622 error_type: error_type.into(),
623 error_message: error_message.into(),
624 stack_trace: Some(stack_trace.into()),
625 }
626 }
627}
628
629impl From<&DurableError> for ErrorObject {
630 fn from(error: &DurableError) -> Self {
631 match error {
632 DurableError::Execution { message, .. } => ErrorObject::new("ExecutionError", message),
633 DurableError::Invocation { message, .. } => {
634 ErrorObject::new("InvocationError", message)
635 }
636 DurableError::Checkpoint { message, .. } => {
637 ErrorObject::new("CheckpointError", message)
638 }
639 DurableError::Callback { message, .. } => ErrorObject::new("CallbackError", message),
640 DurableError::NonDeterministic { message, .. } => {
641 ErrorObject::new("NonDeterministicExecutionError", message)
642 }
643 DurableError::Validation { message } => ErrorObject::new("ValidationError", message),
644 DurableError::SerDes { message } => ErrorObject::new("SerDesError", message),
645 DurableError::Suspend { .. } => {
646 ErrorObject::new("SuspendExecution", "Execution suspended")
647 }
648 DurableError::OrphanedChild { message, .. } => {
649 ErrorObject::new("OrphanedChildError", message)
650 }
651 DurableError::UserCode {
652 message,
653 error_type,
654 stack_trace,
655 } => {
656 let mut obj = ErrorObject::new(error_type, message);
657 obj.stack_trace = stack_trace.clone();
658 obj
659 }
660 DurableError::SizeLimit {
661 message,
662 actual_size,
663 max_size,
664 } => {
665 let detailed_message = match (actual_size, max_size) {
666 (Some(actual), Some(max)) => {
667 format!("{} (actual: {} bytes, max: {} bytes)", message, actual, max)
668 }
669 _ => message.clone(),
670 };
671 ErrorObject::new("SizeLimitExceededError", detailed_message)
672 }
673 DurableError::Throttling {
674 message,
675 retry_after_ms,
676 } => {
677 let detailed_message = match retry_after_ms {
678 Some(ms) => format!("{} (retry after: {}ms)", message, ms),
679 None => message.clone(),
680 };
681 ErrorObject::new("ThrottlingError", detailed_message)
682 }
683 DurableError::ResourceNotFound {
684 message,
685 resource_id,
686 } => {
687 let detailed_message = match resource_id {
688 Some(id) => format!("{} (resource: {})", message, id),
689 None => message.clone(),
690 };
691 ErrorObject::new("ResourceNotFoundError", detailed_message)
692 }
693 DurableError::Configuration { message } => {
694 ErrorObject::new("ConfigurationError", message)
695 }
696 }
697 }
698}
699
700impl From<serde_json::Error> for DurableError {
703 fn from(error: serde_json::Error) -> Self {
704 Self::SerDes {
705 message: error.to_string(),
706 }
707 }
708}
709
710impl From<std::io::Error> for DurableError {
711 fn from(error: std::io::Error) -> Self {
712 Self::Execution {
713 message: error.to_string(),
714 termination_reason: TerminationReason::UnhandledError,
715 }
716 }
717}
718
719impl From<Box<dyn std::error::Error + Send + Sync>> for DurableError {
720 fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
721 Self::UserCode {
722 message: error.to_string(),
723 error_type: "UserCodeError".to_string(),
724 stack_trace: None,
725 }
726 }
727}
728
729impl From<Box<dyn std::error::Error>> for DurableError {
730 fn from(error: Box<dyn std::error::Error>) -> Self {
731 Self::UserCode {
732 message: error.to_string(),
733 error_type: "UserCodeError".to_string(),
734 stack_trace: None,
735 }
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use proptest::prelude::*;
743
744 fn non_empty_string_strategy() -> impl Strategy<Value = String> {
750 "[a-zA-Z0-9_ ]{1,64}".prop_map(|s| s)
751 }
752
753 fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
755 prop_oneof![Just(None), non_empty_string_strategy().prop_map(Some),]
756 }
757
758 fn optional_usize_strategy() -> impl Strategy<Value = Option<usize>> {
760 prop_oneof![Just(None), (1usize..10_000_000usize).prop_map(Some),]
761 }
762
763 fn optional_u64_strategy() -> impl Strategy<Value = Option<u64>> {
765 prop_oneof![Just(None), (1u64..100_000u64).prop_map(Some),]
766 }
767
768 fn execution_error_strategy() -> impl Strategy<Value = DurableError> {
770 non_empty_string_strategy().prop_map(|message| DurableError::Execution {
771 message,
772 termination_reason: TerminationReason::ExecutionError,
773 })
774 }
775
776 fn invocation_error_strategy() -> impl Strategy<Value = DurableError> {
778 non_empty_string_strategy().prop_map(|message| DurableError::Invocation {
779 message,
780 termination_reason: TerminationReason::InvocationError,
781 })
782 }
783
784 fn checkpoint_error_strategy() -> impl Strategy<Value = DurableError> {
786 (non_empty_string_strategy(), any::<bool>()).prop_map(|(message, is_retriable)| {
787 DurableError::Checkpoint {
788 message,
789 is_retriable,
790 aws_error: None,
791 }
792 })
793 }
794
795 fn callback_error_strategy() -> impl Strategy<Value = DurableError> {
797 (non_empty_string_strategy(), optional_string_strategy()).prop_map(
798 |(message, callback_id)| DurableError::Callback {
799 message,
800 callback_id,
801 },
802 )
803 }
804
805 fn non_deterministic_error_strategy() -> impl Strategy<Value = DurableError> {
807 (non_empty_string_strategy(), optional_string_strategy()).prop_map(
808 |(message, operation_id)| DurableError::NonDeterministic {
809 message,
810 operation_id,
811 },
812 )
813 }
814
815 fn validation_error_strategy() -> impl Strategy<Value = DurableError> {
817 non_empty_string_strategy().prop_map(|message| DurableError::Validation { message })
818 }
819
820 fn serdes_error_strategy() -> impl Strategy<Value = DurableError> {
822 non_empty_string_strategy().prop_map(|message| DurableError::SerDes { message })
823 }
824
825 fn suspend_error_strategy() -> impl Strategy<Value = DurableError> {
827 prop_oneof![
828 Just(()).prop_map(|_| DurableError::Suspend {
830 scheduled_timestamp: None
831 }),
832 (0.0f64..1e15f64).prop_map(|ts| DurableError::Suspend {
833 scheduled_timestamp: Some(ts)
834 }),
835 ]
836 }
837
838 fn orphaned_child_error_strategy() -> impl Strategy<Value = DurableError> {
840 (non_empty_string_strategy(), non_empty_string_strategy()).prop_map(
841 |(message, operation_id)| DurableError::OrphanedChild {
842 message,
843 operation_id,
844 },
845 )
846 }
847
848 fn user_code_error_strategy() -> impl Strategy<Value = DurableError> {
850 (
851 non_empty_string_strategy(),
852 non_empty_string_strategy(),
853 optional_string_strategy(),
854 )
855 .prop_map(
856 |(message, error_type, stack_trace)| DurableError::UserCode {
857 message,
858 error_type,
859 stack_trace,
860 },
861 )
862 }
863
864 fn size_limit_error_strategy() -> impl Strategy<Value = DurableError> {
866 (
867 non_empty_string_strategy(),
868 optional_usize_strategy(),
869 optional_usize_strategy(),
870 )
871 .prop_map(|(message, actual_size, max_size)| DurableError::SizeLimit {
872 message,
873 actual_size,
874 max_size,
875 })
876 }
877
878 fn throttling_error_strategy() -> impl Strategy<Value = DurableError> {
880 (non_empty_string_strategy(), optional_u64_strategy()).prop_map(
881 |(message, retry_after_ms)| DurableError::Throttling {
882 message,
883 retry_after_ms,
884 },
885 )
886 }
887
888 fn resource_not_found_error_strategy() -> impl Strategy<Value = DurableError> {
890 (non_empty_string_strategy(), optional_string_strategy()).prop_map(
891 |(message, resource_id)| DurableError::ResourceNotFound {
892 message,
893 resource_id,
894 },
895 )
896 }
897
898 fn durable_error_strategy() -> impl Strategy<Value = DurableError> {
900 prop_oneof![
901 execution_error_strategy(),
902 invocation_error_strategy(),
903 checkpoint_error_strategy(),
904 callback_error_strategy(),
905 non_deterministic_error_strategy(),
906 validation_error_strategy(),
907 serdes_error_strategy(),
908 suspend_error_strategy(),
909 orphaned_child_error_strategy(),
910 user_code_error_strategy(),
911 size_limit_error_strategy(),
912 throttling_error_strategy(),
913 resource_not_found_error_strategy(),
914 ]
915 }
916
917 proptest! {
922 #[test]
927 fn prop_durable_error_to_error_object_produces_valid_fields(error in durable_error_strategy()) {
928 let error_object: ErrorObject = (&error).into();
929
930 prop_assert!(
932 !error_object.error_type.is_empty(),
933 "ErrorObject.error_type should be non-empty for {:?}",
934 error
935 );
936
937 prop_assert!(
939 !error_object.error_message.is_empty(),
940 "ErrorObject.error_message should be non-empty for {:?}",
941 error
942 );
943 }
944
945 #[test]
949 fn prop_size_limit_error_classification(error in size_limit_error_strategy()) {
950 prop_assert!(
951 error.is_size_limit(),
952 "SizeLimit error should return true for is_size_limit()"
953 );
954 prop_assert!(
955 !error.is_retriable(),
956 "SizeLimit error should return false for is_retriable()"
957 );
958 prop_assert!(
959 !error.is_throttling(),
960 "SizeLimit error should return false for is_throttling()"
961 );
962 prop_assert!(
963 !error.is_resource_not_found(),
964 "SizeLimit error should return false for is_resource_not_found()"
965 );
966 }
967
968 #[test]
972 fn prop_throttling_error_classification(error in throttling_error_strategy()) {
973 prop_assert!(
974 error.is_throttling(),
975 "Throttling error should return true for is_throttling()"
976 );
977 prop_assert!(
978 !error.is_size_limit(),
979 "Throttling error should return false for is_size_limit()"
980 );
981 prop_assert!(
982 !error.is_resource_not_found(),
983 "Throttling error should return false for is_resource_not_found()"
984 );
985 }
986
987 #[test]
991 fn prop_resource_not_found_error_classification(error in resource_not_found_error_strategy()) {
992 prop_assert!(
993 error.is_resource_not_found(),
994 "ResourceNotFound error should return true for is_resource_not_found()"
995 );
996 prop_assert!(
997 !error.is_retriable(),
998 "ResourceNotFound error should return false for is_retriable()"
999 );
1000 prop_assert!(
1001 !error.is_size_limit(),
1002 "ResourceNotFound error should return false for is_size_limit()"
1003 );
1004 prop_assert!(
1005 !error.is_throttling(),
1006 "ResourceNotFound error should return false for is_throttling()"
1007 );
1008 }
1009
1010 #[test]
1014 fn prop_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
1015 let error = DurableError::Checkpoint {
1016 message,
1017 is_retriable: true,
1018 aws_error: None,
1019 };
1020 prop_assert!(
1021 error.is_retriable(),
1022 "Checkpoint error with is_retriable=true should return true for is_retriable()"
1023 );
1024 }
1025
1026 #[test]
1030 fn prop_non_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
1031 let error = DurableError::Checkpoint {
1032 message,
1033 is_retriable: false,
1034 aws_error: None,
1035 };
1036 prop_assert!(
1037 !error.is_retriable(),
1038 "Checkpoint error with is_retriable=false should return false for is_retriable()"
1039 );
1040 }
1041
1042 #[test]
1047 fn prop_error_object_type_matches_variant(error in durable_error_strategy()) {
1048 let error_object: ErrorObject = (&error).into();
1049
1050 let expected_type = match &error {
1051 DurableError::Execution { .. } => "ExecutionError",
1052 DurableError::Invocation { .. } => "InvocationError",
1053 DurableError::Checkpoint { .. } => "CheckpointError",
1054 DurableError::Callback { .. } => "CallbackError",
1055 DurableError::NonDeterministic { .. } => "NonDeterministicExecutionError",
1056 DurableError::Validation { .. } => "ValidationError",
1057 DurableError::SerDes { .. } => "SerDesError",
1058 DurableError::Suspend { .. } => "SuspendExecution",
1059 DurableError::OrphanedChild { .. } => "OrphanedChildError",
1060 DurableError::UserCode { error_type, .. } => error_type.as_str(),
1061 DurableError::SizeLimit { .. } => "SizeLimitExceededError",
1062 DurableError::Throttling { .. } => "ThrottlingError",
1063 DurableError::ResourceNotFound { .. } => "ResourceNotFoundError",
1064 DurableError::Configuration { .. } => "ConfigurationError",
1065 };
1066
1067 prop_assert_eq!(
1068 error_object.error_type,
1069 expected_type,
1070 "ErrorObject.error_type should match expected type for {:?}",
1071 error
1072 );
1073 }
1074 }
1075
1076 #[test]
1081 fn test_execution_error() {
1082 let error = DurableError::execution("test error");
1083 assert!(matches!(error, DurableError::Execution { .. }));
1084 assert!(!error.is_retriable());
1085 assert!(!error.is_suspend());
1086 }
1087
1088 #[test]
1089 fn test_checkpoint_retriable() {
1090 let error = DurableError::checkpoint_retriable("test error");
1091 assert!(error.is_retriable());
1092 }
1093
1094 #[test]
1095 fn test_checkpoint_non_retriable() {
1096 let error = DurableError::checkpoint_non_retriable("test error");
1097 assert!(!error.is_retriable());
1098 }
1099
1100 #[test]
1101 fn test_suspend() {
1102 let error = DurableError::suspend();
1103 assert!(error.is_suspend());
1104 }
1105
1106 #[test]
1107 fn test_suspend_until() {
1108 let error = DurableError::suspend_until(1234567890.0);
1109 assert!(error.is_suspend());
1110 if let DurableError::Suspend {
1111 scheduled_timestamp,
1112 } = error
1113 {
1114 assert_eq!(scheduled_timestamp, Some(1234567890.0));
1115 }
1116 }
1117
1118 #[test]
1119 fn test_error_object_from_durable_error() {
1120 let error = DurableError::validation("invalid input");
1121 let obj: ErrorObject = (&error).into();
1122 assert_eq!(obj.error_type, "ValidationError");
1123 assert_eq!(obj.error_message, "invalid input");
1124 }
1125
1126 #[test]
1127 fn test_from_serde_json_error() {
1128 let json_error = serde_json::from_str::<String>("invalid").unwrap_err();
1129 let error: DurableError = json_error.into();
1130 assert!(matches!(error, DurableError::SerDes { .. }));
1131 }
1132
1133 #[test]
1134 fn test_is_invalid_checkpoint_token_with_aws_error() {
1135 let error = DurableError::Checkpoint {
1136 message: "Checkpoint API returned 400: Invalid checkpoint token".to_string(),
1137 is_retriable: true,
1138 aws_error: Some(AwsError {
1139 code: "InvalidParameterValueException".to_string(),
1140 message: "Invalid checkpoint token: token has been consumed".to_string(),
1141 request_id: None,
1142 }),
1143 };
1144 assert!(error.is_invalid_checkpoint_token());
1145 assert!(error.is_retriable());
1146 }
1147
1148 #[test]
1149 fn test_is_invalid_checkpoint_token_without_aws_error() {
1150 let error = DurableError::Checkpoint {
1151 message: "Invalid checkpoint token: token expired".to_string(),
1152 is_retriable: true,
1153 aws_error: None,
1154 };
1155 assert!(error.is_invalid_checkpoint_token());
1156 }
1157
1158 #[test]
1159 fn test_is_not_invalid_checkpoint_token() {
1160 let error = DurableError::Checkpoint {
1161 message: "Network error".to_string(),
1162 is_retriable: true,
1163 aws_error: None,
1164 };
1165 assert!(!error.is_invalid_checkpoint_token());
1166 }
1167
1168 #[test]
1169 fn test_is_invalid_checkpoint_token_wrong_error_type() {
1170 let error = DurableError::Validation {
1171 message: "Invalid checkpoint token".to_string(),
1172 };
1173 assert!(!error.is_invalid_checkpoint_token());
1174 }
1175
1176 #[test]
1177 fn test_is_invalid_checkpoint_token_wrong_aws_error_code() {
1178 let error = DurableError::Checkpoint {
1179 message: "Some error".to_string(),
1180 is_retriable: false,
1181 aws_error: Some(AwsError {
1182 code: "ServiceException".to_string(),
1183 message: "Invalid checkpoint token".to_string(),
1184 request_id: None,
1185 }),
1186 };
1187 assert!(!error.is_invalid_checkpoint_token());
1189 }
1190
1191 #[test]
1192 fn test_size_limit_error() {
1193 let error = DurableError::size_limit("Payload too large");
1194 assert!(error.is_size_limit());
1195 assert!(!error.is_retriable());
1196 assert!(!error.is_throttling());
1197 assert!(!error.is_resource_not_found());
1198 }
1199
1200 #[test]
1201 fn test_size_limit_error_with_details() {
1202 let error =
1203 DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
1204 assert!(error.is_size_limit());
1205 if let DurableError::SizeLimit {
1206 actual_size,
1207 max_size,
1208 ..
1209 } = error
1210 {
1211 assert_eq!(actual_size, Some(7_000_000));
1212 assert_eq!(max_size, Some(6_000_000));
1213 } else {
1214 panic!("Expected SizeLimit error");
1215 }
1216 }
1217
1218 #[test]
1219 fn test_throttling_error() {
1220 let error = DurableError::throttling("Rate limit exceeded");
1221 assert!(error.is_throttling());
1222 assert!(!error.is_retriable());
1223 assert!(!error.is_size_limit());
1224 assert!(!error.is_resource_not_found());
1225 assert_eq!(error.get_retry_after_ms(), None);
1226 }
1227
1228 #[test]
1229 fn test_throttling_error_with_retry_delay() {
1230 let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
1231 assert!(error.is_throttling());
1232 assert_eq!(error.get_retry_after_ms(), Some(5000));
1233 }
1234
1235 #[test]
1236 fn test_resource_not_found_error() {
1237 let error = DurableError::resource_not_found("Execution not found");
1238 assert!(error.is_resource_not_found());
1239 assert!(!error.is_retriable());
1240 assert!(!error.is_size_limit());
1241 assert!(!error.is_throttling());
1242 }
1243
1244 #[test]
1245 fn test_resource_not_found_error_with_id() {
1246 let error = DurableError::resource_not_found_with_id(
1247 "Execution not found",
1248 "arn:aws:lambda:us-east-1:123456789012:function:test",
1249 );
1250 assert!(error.is_resource_not_found());
1251 if let DurableError::ResourceNotFound { resource_id, .. } = error {
1252 assert_eq!(
1253 resource_id,
1254 Some("arn:aws:lambda:us-east-1:123456789012:function:test".to_string())
1255 );
1256 } else {
1257 panic!("Expected ResourceNotFound error");
1258 }
1259 }
1260
1261 #[test]
1262 fn test_error_object_from_size_limit_error() {
1263 let error =
1264 DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
1265 let obj: ErrorObject = (&error).into();
1266 assert_eq!(obj.error_type, "SizeLimitExceededError");
1267 assert!(obj.error_message.contains("Payload too large"));
1268 assert!(obj.error_message.contains("7000000"));
1269 assert!(obj.error_message.contains("6000000"));
1270 }
1271
1272 #[test]
1273 fn test_error_object_from_throttling_error() {
1274 let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
1275 let obj: ErrorObject = (&error).into();
1276 assert_eq!(obj.error_type, "ThrottlingError");
1277 assert!(obj.error_message.contains("Rate limit exceeded"));
1278 assert!(obj.error_message.contains("5000ms"));
1279 }
1280
1281 #[test]
1282 fn test_error_object_from_resource_not_found_error() {
1283 let error = DurableError::resource_not_found_with_id("Execution not found", "test-arn");
1284 let obj: ErrorObject = (&error).into();
1285 assert_eq!(obj.error_type, "ResourceNotFoundError");
1286 assert!(obj.error_message.contains("Execution not found"));
1287 assert!(obj.error_message.contains("test-arn"));
1288 }
1289
1290 #[test]
1291 fn test_get_retry_after_ms_non_throttling() {
1292 let error = DurableError::validation("test");
1293 assert_eq!(error.get_retry_after_ms(), None);
1294 }
1295
1296 #[test]
1300 fn test_termination_reason_size_is_one_byte() {
1301 assert_eq!(
1302 std::mem::size_of::<TerminationReason>(),
1303 1,
1304 "TerminationReason should be 1 byte with #[repr(u8)]"
1305 );
1306 }
1307
1308 #[test]
1310 fn test_termination_reason_discriminant_values() {
1311 assert_eq!(TerminationReason::UnhandledError as u8, 0);
1313 assert_eq!(TerminationReason::InvocationError as u8, 1);
1314 assert_eq!(TerminationReason::ExecutionError as u8, 2);
1315 assert_eq!(TerminationReason::CheckpointFailed as u8, 3);
1316 assert_eq!(TerminationReason::NonDeterministicExecution as u8, 4);
1317 assert_eq!(TerminationReason::StepInterrupted as u8, 5);
1318 assert_eq!(TerminationReason::CallbackError as u8, 6);
1319 assert_eq!(TerminationReason::SerializationError as u8, 7);
1320 assert_eq!(TerminationReason::SizeLimitExceeded as u8, 8);
1321
1322 assert_eq!(TerminationReason::OperationTerminated as u8, 9);
1324 assert_eq!(TerminationReason::RetryScheduled as u8, 10);
1325 assert_eq!(TerminationReason::WaitScheduled as u8, 11);
1326 assert_eq!(TerminationReason::CallbackPending as u8, 12);
1327 assert_eq!(TerminationReason::ContextValidationError as u8, 13);
1328 assert_eq!(TerminationReason::LambdaTimeoutApproaching as u8, 14);
1329 }
1330
1331 #[test]
1335 fn test_termination_reason_serde_uses_string_representation() {
1336 let reason = TerminationReason::UnhandledError;
1338 let json = serde_json::to_string(&reason).unwrap();
1339 assert_eq!(json, "\"UnhandledError\"");
1340
1341 let reason = TerminationReason::InvocationError;
1342 let json = serde_json::to_string(&reason).unwrap();
1343 assert_eq!(json, "\"InvocationError\"");
1344
1345 let reason = TerminationReason::ExecutionError;
1346 let json = serde_json::to_string(&reason).unwrap();
1347 assert_eq!(json, "\"ExecutionError\"");
1348
1349 let reason = TerminationReason::CheckpointFailed;
1350 let json = serde_json::to_string(&reason).unwrap();
1351 assert_eq!(json, "\"CheckpointFailed\"");
1352
1353 let reason = TerminationReason::NonDeterministicExecution;
1354 let json = serde_json::to_string(&reason).unwrap();
1355 assert_eq!(json, "\"NonDeterministicExecution\"");
1356
1357 let reason = TerminationReason::StepInterrupted;
1358 let json = serde_json::to_string(&reason).unwrap();
1359 assert_eq!(json, "\"StepInterrupted\"");
1360
1361 let reason = TerminationReason::CallbackError;
1362 let json = serde_json::to_string(&reason).unwrap();
1363 assert_eq!(json, "\"CallbackError\"");
1364
1365 let reason = TerminationReason::SerializationError;
1366 let json = serde_json::to_string(&reason).unwrap();
1367 assert_eq!(json, "\"SerializationError\"");
1368
1369 let reason = TerminationReason::SizeLimitExceeded;
1370 let json = serde_json::to_string(&reason).unwrap();
1371 assert_eq!(json, "\"SizeLimitExceeded\"");
1372
1373 let reason = TerminationReason::OperationTerminated;
1374 let json = serde_json::to_string(&reason).unwrap();
1375 assert_eq!(json, "\"OperationTerminated\"");
1376
1377 let reason = TerminationReason::RetryScheduled;
1378 let json = serde_json::to_string(&reason).unwrap();
1379 assert_eq!(json, "\"RetryScheduled\"");
1380
1381 let reason = TerminationReason::WaitScheduled;
1382 let json = serde_json::to_string(&reason).unwrap();
1383 assert_eq!(json, "\"WaitScheduled\"");
1384
1385 let reason = TerminationReason::CallbackPending;
1386 let json = serde_json::to_string(&reason).unwrap();
1387 assert_eq!(json, "\"CallbackPending\"");
1388
1389 let reason = TerminationReason::ContextValidationError;
1390 let json = serde_json::to_string(&reason).unwrap();
1391 assert_eq!(json, "\"ContextValidationError\"");
1392
1393 let reason = TerminationReason::LambdaTimeoutApproaching;
1394 let json = serde_json::to_string(&reason).unwrap();
1395 assert_eq!(json, "\"LambdaTimeoutApproaching\"");
1396 }
1397
1398 #[test]
1399 fn test_termination_reason_serde_round_trip() {
1400 let reasons = [
1401 TerminationReason::UnhandledError,
1402 TerminationReason::InvocationError,
1403 TerminationReason::ExecutionError,
1404 TerminationReason::CheckpointFailed,
1405 TerminationReason::NonDeterministicExecution,
1406 TerminationReason::StepInterrupted,
1407 TerminationReason::CallbackError,
1408 TerminationReason::SerializationError,
1409 TerminationReason::SizeLimitExceeded,
1410 TerminationReason::OperationTerminated,
1411 TerminationReason::RetryScheduled,
1412 TerminationReason::WaitScheduled,
1413 TerminationReason::CallbackPending,
1414 TerminationReason::ContextValidationError,
1415 TerminationReason::LambdaTimeoutApproaching,
1416 ];
1417
1418 for reason in reasons {
1419 let json = serde_json::to_string(&reason).unwrap();
1420 let deserialized: TerminationReason = serde_json::from_str(&json).unwrap();
1421 assert_eq!(reason, deserialized, "Round-trip failed for {:?}", reason);
1422 }
1423 }
1424}