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}")]
244 SizeLimit {
245 message: String,
247 actual_size: Option<usize>,
249 max_size: Option<usize>,
251 },
252
253 #[error("Throttling: {message}")]
262 Throttling {
263 message: String,
265 retry_after_ms: Option<u64>,
267 },
268
269 #[error("Resource not found: {message}")]
282 ResourceNotFound {
283 message: String,
285 resource_id: Option<String>,
287 },
288}
289
290impl DurableError {
291 pub fn execution(message: impl Into<String>) -> Self {
293 Self::Execution {
294 message: message.into(),
295 termination_reason: TerminationReason::ExecutionError,
296 }
297 }
298
299 pub fn invocation(message: impl Into<String>) -> Self {
301 Self::Invocation {
302 message: message.into(),
303 termination_reason: TerminationReason::InvocationError,
304 }
305 }
306
307 pub fn checkpoint_retriable(message: impl Into<String>) -> Self {
309 Self::Checkpoint {
310 message: message.into(),
311 is_retriable: true,
312 aws_error: None,
313 }
314 }
315
316 pub fn checkpoint_non_retriable(message: impl Into<String>) -> Self {
318 Self::Checkpoint {
319 message: message.into(),
320 is_retriable: false,
321 aws_error: None,
322 }
323 }
324
325 pub fn validation(message: impl Into<String>) -> Self {
327 Self::Validation {
328 message: message.into(),
329 }
330 }
331
332 pub fn serdes(message: impl Into<String>) -> Self {
334 Self::SerDes {
335 message: message.into(),
336 }
337 }
338
339 pub fn suspend() -> Self {
341 Self::Suspend {
342 scheduled_timestamp: None,
343 }
344 }
345
346 pub fn suspend_until(timestamp: f64) -> Self {
348 Self::Suspend {
349 scheduled_timestamp: Some(timestamp),
350 }
351 }
352
353 pub fn size_limit(message: impl Into<String>) -> Self {
363 Self::SizeLimit {
364 message: message.into(),
365 actual_size: None,
366 max_size: None,
367 }
368 }
369
370 pub fn size_limit_with_details(
382 message: impl Into<String>,
383 actual_size: usize,
384 max_size: usize,
385 ) -> Self {
386 Self::SizeLimit {
387 message: message.into(),
388 actual_size: Some(actual_size),
389 max_size: Some(max_size),
390 }
391 }
392
393 pub fn throttling(message: impl Into<String>) -> Self {
403 Self::Throttling {
404 message: message.into(),
405 retry_after_ms: None,
406 }
407 }
408
409 pub fn throttling_with_retry_delay(message: impl Into<String>, retry_after_ms: u64) -> Self {
420 Self::Throttling {
421 message: message.into(),
422 retry_after_ms: Some(retry_after_ms),
423 }
424 }
425
426 pub fn resource_not_found(message: impl Into<String>) -> Self {
436 Self::ResourceNotFound {
437 message: message.into(),
438 resource_id: None,
439 }
440 }
441
442 pub fn resource_not_found_with_id(
453 message: impl Into<String>,
454 resource_id: impl Into<String>,
455 ) -> Self {
456 Self::ResourceNotFound {
457 message: message.into(),
458 resource_id: Some(resource_id.into()),
459 }
460 }
461
462 pub fn is_retriable(&self) -> bool {
464 matches!(
465 self,
466 Self::Checkpoint {
467 is_retriable: true,
468 ..
469 }
470 )
471 }
472
473 pub fn is_suspend(&self) -> bool {
475 matches!(self, Self::Suspend { .. })
476 }
477
478 pub fn is_invalid_checkpoint_token(&self) -> bool {
491 match self {
492 Self::Checkpoint {
493 aws_error: Some(aws_error),
494 ..
495 } => {
496 aws_error.code == "InvalidParameterValueException"
497 && aws_error.message.contains("Invalid checkpoint token")
498 }
499 Self::Checkpoint { message, .. } => message.contains("Invalid checkpoint token"),
500 _ => false,
501 }
502 }
503
504 pub fn is_size_limit(&self) -> bool {
508 matches!(self, Self::SizeLimit { .. })
509 }
510
511 pub fn is_throttling(&self) -> bool {
515 matches!(self, Self::Throttling { .. })
516 }
517
518 pub fn is_resource_not_found(&self) -> bool {
522 matches!(self, Self::ResourceNotFound { .. })
523 }
524
525 pub fn get_retry_after_ms(&self) -> Option<u64> {
529 match self {
530 Self::Throttling { retry_after_ms, .. } => *retry_after_ms,
531 _ => None,
532 }
533 }
534}
535
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
541#[repr(u8)]
542pub enum TerminationReason {
543 #[default]
545 UnhandledError = 0,
546 InvocationError = 1,
548 ExecutionError = 2,
550 CheckpointFailed = 3,
552 NonDeterministicExecution = 4,
554 StepInterrupted = 5,
556 CallbackError = 6,
558 SerializationError = 7,
560 SizeLimitExceeded = 8,
562 OperationTerminated = 9,
564 RetryScheduled = 10,
566 WaitScheduled = 11,
568 CallbackPending = 12,
570 ContextValidationError = 13,
572 LambdaTimeoutApproaching = 14,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct AwsError {
579 pub code: String,
581 pub message: String,
583 pub request_id: Option<String>,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct ErrorObject {
621 #[serde(rename = "ErrorType")]
623 pub error_type: String,
624 #[serde(rename = "ErrorMessage")]
626 pub error_message: String,
627 #[serde(rename = "StackTrace", skip_serializing_if = "Option::is_none")]
629 pub stack_trace: Option<String>,
630}
631
632impl ErrorObject {
633 pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
635 Self {
636 error_type: error_type.into(),
637 error_message: error_message.into(),
638 stack_trace: None,
639 }
640 }
641
642 pub fn with_stack_trace(
644 error_type: impl Into<String>,
645 error_message: impl Into<String>,
646 stack_trace: impl Into<String>,
647 ) -> Self {
648 Self {
649 error_type: error_type.into(),
650 error_message: error_message.into(),
651 stack_trace: Some(stack_trace.into()),
652 }
653 }
654}
655
656impl From<&DurableError> for ErrorObject {
657 fn from(error: &DurableError) -> Self {
658 match error {
659 DurableError::Execution { message, .. } => ErrorObject::new("ExecutionError", message),
660 DurableError::Invocation { message, .. } => {
661 ErrorObject::new("InvocationError", message)
662 }
663 DurableError::Checkpoint { message, .. } => {
664 ErrorObject::new("CheckpointError", message)
665 }
666 DurableError::Callback { message, .. } => ErrorObject::new("CallbackError", message),
667 DurableError::NonDeterministic { message, .. } => {
668 ErrorObject::new("NonDeterministicExecutionError", message)
669 }
670 DurableError::Validation { message } => ErrorObject::new("ValidationError", message),
671 DurableError::SerDes { message } => ErrorObject::new("SerDesError", message),
672 DurableError::Suspend { .. } => {
673 ErrorObject::new("SuspendExecution", "Execution suspended")
674 }
675 DurableError::OrphanedChild { message, .. } => {
676 ErrorObject::new("OrphanedChildError", message)
677 }
678 DurableError::UserCode {
679 message,
680 error_type,
681 stack_trace,
682 } => {
683 let mut obj = ErrorObject::new(error_type, message);
684 obj.stack_trace = stack_trace.clone();
685 obj
686 }
687 DurableError::SizeLimit {
688 message,
689 actual_size,
690 max_size,
691 } => {
692 let detailed_message = match (actual_size, max_size) {
693 (Some(actual), Some(max)) => {
694 format!("{} (actual: {} bytes, max: {} bytes)", message, actual, max)
695 }
696 _ => message.clone(),
697 };
698 ErrorObject::new("SizeLimitExceededError", detailed_message)
699 }
700 DurableError::Throttling {
701 message,
702 retry_after_ms,
703 } => {
704 let detailed_message = match retry_after_ms {
705 Some(ms) => format!("{} (retry after: {}ms)", message, ms),
706 None => message.clone(),
707 };
708 ErrorObject::new("ThrottlingError", detailed_message)
709 }
710 DurableError::ResourceNotFound {
711 message,
712 resource_id,
713 } => {
714 let detailed_message = match resource_id {
715 Some(id) => format!("{} (resource: {})", message, id),
716 None => message.clone(),
717 };
718 ErrorObject::new("ResourceNotFoundError", detailed_message)
719 }
720 }
721 }
722}
723
724impl From<serde_json::Error> for DurableError {
727 fn from(error: serde_json::Error) -> Self {
728 Self::SerDes {
729 message: error.to_string(),
730 }
731 }
732}
733
734impl From<std::io::Error> for DurableError {
735 fn from(error: std::io::Error) -> Self {
736 Self::Execution {
737 message: error.to_string(),
738 termination_reason: TerminationReason::UnhandledError,
739 }
740 }
741}
742
743impl From<Box<dyn std::error::Error + Send + Sync>> for DurableError {
744 fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
745 Self::UserCode {
746 message: error.to_string(),
747 error_type: "UserCodeError".to_string(),
748 stack_trace: None,
749 }
750 }
751}
752
753impl From<Box<dyn std::error::Error>> for DurableError {
754 fn from(error: Box<dyn std::error::Error>) -> Self {
755 Self::UserCode {
756 message: error.to_string(),
757 error_type: "UserCodeError".to_string(),
758 stack_trace: None,
759 }
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use proptest::prelude::*;
767
768 fn non_empty_string_strategy() -> impl Strategy<Value = String> {
774 "[a-zA-Z0-9_ ]{1,64}".prop_map(|s| s)
775 }
776
777 fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
779 prop_oneof![Just(None), non_empty_string_strategy().prop_map(Some),]
780 }
781
782 fn optional_usize_strategy() -> impl Strategy<Value = Option<usize>> {
784 prop_oneof![Just(None), (1usize..10_000_000usize).prop_map(Some),]
785 }
786
787 fn optional_u64_strategy() -> impl Strategy<Value = Option<u64>> {
789 prop_oneof![Just(None), (1u64..100_000u64).prop_map(Some),]
790 }
791
792 fn execution_error_strategy() -> impl Strategy<Value = DurableError> {
794 non_empty_string_strategy().prop_map(|message| DurableError::Execution {
795 message,
796 termination_reason: TerminationReason::ExecutionError,
797 })
798 }
799
800 fn invocation_error_strategy() -> impl Strategy<Value = DurableError> {
802 non_empty_string_strategy().prop_map(|message| DurableError::Invocation {
803 message,
804 termination_reason: TerminationReason::InvocationError,
805 })
806 }
807
808 fn checkpoint_error_strategy() -> impl Strategy<Value = DurableError> {
810 (non_empty_string_strategy(), any::<bool>()).prop_map(|(message, is_retriable)| {
811 DurableError::Checkpoint {
812 message,
813 is_retriable,
814 aws_error: None,
815 }
816 })
817 }
818
819 fn callback_error_strategy() -> impl Strategy<Value = DurableError> {
821 (non_empty_string_strategy(), optional_string_strategy()).prop_map(
822 |(message, callback_id)| DurableError::Callback {
823 message,
824 callback_id,
825 },
826 )
827 }
828
829 fn non_deterministic_error_strategy() -> impl Strategy<Value = DurableError> {
831 (non_empty_string_strategy(), optional_string_strategy()).prop_map(
832 |(message, operation_id)| DurableError::NonDeterministic {
833 message,
834 operation_id,
835 },
836 )
837 }
838
839 fn validation_error_strategy() -> impl Strategy<Value = DurableError> {
841 non_empty_string_strategy().prop_map(|message| DurableError::Validation { message })
842 }
843
844 fn serdes_error_strategy() -> impl Strategy<Value = DurableError> {
846 non_empty_string_strategy().prop_map(|message| DurableError::SerDes { message })
847 }
848
849 fn suspend_error_strategy() -> impl Strategy<Value = DurableError> {
851 prop_oneof![
852 Just(()).prop_map(|_| DurableError::Suspend {
854 scheduled_timestamp: None
855 }),
856 (0.0f64..1e15f64).prop_map(|ts| DurableError::Suspend {
857 scheduled_timestamp: Some(ts)
858 }),
859 ]
860 }
861
862 fn orphaned_child_error_strategy() -> impl Strategy<Value = DurableError> {
864 (non_empty_string_strategy(), non_empty_string_strategy()).prop_map(
865 |(message, operation_id)| DurableError::OrphanedChild {
866 message,
867 operation_id,
868 },
869 )
870 }
871
872 fn user_code_error_strategy() -> impl Strategy<Value = DurableError> {
874 (
875 non_empty_string_strategy(),
876 non_empty_string_strategy(),
877 optional_string_strategy(),
878 )
879 .prop_map(
880 |(message, error_type, stack_trace)| DurableError::UserCode {
881 message,
882 error_type,
883 stack_trace,
884 },
885 )
886 }
887
888 fn size_limit_error_strategy() -> impl Strategy<Value = DurableError> {
890 (
891 non_empty_string_strategy(),
892 optional_usize_strategy(),
893 optional_usize_strategy(),
894 )
895 .prop_map(|(message, actual_size, max_size)| DurableError::SizeLimit {
896 message,
897 actual_size,
898 max_size,
899 })
900 }
901
902 fn throttling_error_strategy() -> impl Strategy<Value = DurableError> {
904 (non_empty_string_strategy(), optional_u64_strategy()).prop_map(
905 |(message, retry_after_ms)| DurableError::Throttling {
906 message,
907 retry_after_ms,
908 },
909 )
910 }
911
912 fn resource_not_found_error_strategy() -> impl Strategy<Value = DurableError> {
914 (non_empty_string_strategy(), optional_string_strategy()).prop_map(
915 |(message, resource_id)| DurableError::ResourceNotFound {
916 message,
917 resource_id,
918 },
919 )
920 }
921
922 fn durable_error_strategy() -> impl Strategy<Value = DurableError> {
924 prop_oneof![
925 execution_error_strategy(),
926 invocation_error_strategy(),
927 checkpoint_error_strategy(),
928 callback_error_strategy(),
929 non_deterministic_error_strategy(),
930 validation_error_strategy(),
931 serdes_error_strategy(),
932 suspend_error_strategy(),
933 orphaned_child_error_strategy(),
934 user_code_error_strategy(),
935 size_limit_error_strategy(),
936 throttling_error_strategy(),
937 resource_not_found_error_strategy(),
938 ]
939 }
940
941 proptest! {
946 #[test]
951 fn prop_durable_error_to_error_object_produces_valid_fields(error in durable_error_strategy()) {
952 let error_object: ErrorObject = (&error).into();
953
954 prop_assert!(
956 !error_object.error_type.is_empty(),
957 "ErrorObject.error_type should be non-empty for {:?}",
958 error
959 );
960
961 prop_assert!(
963 !error_object.error_message.is_empty(),
964 "ErrorObject.error_message should be non-empty for {:?}",
965 error
966 );
967 }
968
969 #[test]
973 fn prop_size_limit_error_classification(error in size_limit_error_strategy()) {
974 prop_assert!(
975 error.is_size_limit(),
976 "SizeLimit error should return true for is_size_limit()"
977 );
978 prop_assert!(
979 !error.is_retriable(),
980 "SizeLimit error should return false for is_retriable()"
981 );
982 prop_assert!(
983 !error.is_throttling(),
984 "SizeLimit error should return false for is_throttling()"
985 );
986 prop_assert!(
987 !error.is_resource_not_found(),
988 "SizeLimit error should return false for is_resource_not_found()"
989 );
990 }
991
992 #[test]
996 fn prop_throttling_error_classification(error in throttling_error_strategy()) {
997 prop_assert!(
998 error.is_throttling(),
999 "Throttling error should return true for is_throttling()"
1000 );
1001 prop_assert!(
1002 !error.is_size_limit(),
1003 "Throttling error should return false for is_size_limit()"
1004 );
1005 prop_assert!(
1006 !error.is_resource_not_found(),
1007 "Throttling error should return false for is_resource_not_found()"
1008 );
1009 }
1010
1011 #[test]
1015 fn prop_resource_not_found_error_classification(error in resource_not_found_error_strategy()) {
1016 prop_assert!(
1017 error.is_resource_not_found(),
1018 "ResourceNotFound error should return true for is_resource_not_found()"
1019 );
1020 prop_assert!(
1021 !error.is_retriable(),
1022 "ResourceNotFound error should return false for is_retriable()"
1023 );
1024 prop_assert!(
1025 !error.is_size_limit(),
1026 "ResourceNotFound error should return false for is_size_limit()"
1027 );
1028 prop_assert!(
1029 !error.is_throttling(),
1030 "ResourceNotFound error should return false for is_throttling()"
1031 );
1032 }
1033
1034 #[test]
1038 fn prop_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
1039 let error = DurableError::Checkpoint {
1040 message,
1041 is_retriable: true,
1042 aws_error: None,
1043 };
1044 prop_assert!(
1045 error.is_retriable(),
1046 "Checkpoint error with is_retriable=true should return true for is_retriable()"
1047 );
1048 }
1049
1050 #[test]
1054 fn prop_non_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
1055 let error = DurableError::Checkpoint {
1056 message,
1057 is_retriable: false,
1058 aws_error: None,
1059 };
1060 prop_assert!(
1061 !error.is_retriable(),
1062 "Checkpoint error with is_retriable=false should return false for is_retriable()"
1063 );
1064 }
1065
1066 #[test]
1071 fn prop_error_object_type_matches_variant(error in durable_error_strategy()) {
1072 let error_object: ErrorObject = (&error).into();
1073
1074 let expected_type = match &error {
1075 DurableError::Execution { .. } => "ExecutionError",
1076 DurableError::Invocation { .. } => "InvocationError",
1077 DurableError::Checkpoint { .. } => "CheckpointError",
1078 DurableError::Callback { .. } => "CallbackError",
1079 DurableError::NonDeterministic { .. } => "NonDeterministicExecutionError",
1080 DurableError::Validation { .. } => "ValidationError",
1081 DurableError::SerDes { .. } => "SerDesError",
1082 DurableError::Suspend { .. } => "SuspendExecution",
1083 DurableError::OrphanedChild { .. } => "OrphanedChildError",
1084 DurableError::UserCode { error_type, .. } => error_type.as_str(),
1085 DurableError::SizeLimit { .. } => "SizeLimitExceededError",
1086 DurableError::Throttling { .. } => "ThrottlingError",
1087 DurableError::ResourceNotFound { .. } => "ResourceNotFoundError",
1088 };
1089
1090 prop_assert_eq!(
1091 error_object.error_type,
1092 expected_type,
1093 "ErrorObject.error_type should match expected type for {:?}",
1094 error
1095 );
1096 }
1097 }
1098
1099 #[test]
1104 fn test_execution_error() {
1105 let error = DurableError::execution("test error");
1106 assert!(matches!(error, DurableError::Execution { .. }));
1107 assert!(!error.is_retriable());
1108 assert!(!error.is_suspend());
1109 }
1110
1111 #[test]
1112 fn test_checkpoint_retriable() {
1113 let error = DurableError::checkpoint_retriable("test error");
1114 assert!(error.is_retriable());
1115 }
1116
1117 #[test]
1118 fn test_checkpoint_non_retriable() {
1119 let error = DurableError::checkpoint_non_retriable("test error");
1120 assert!(!error.is_retriable());
1121 }
1122
1123 #[test]
1124 fn test_suspend() {
1125 let error = DurableError::suspend();
1126 assert!(error.is_suspend());
1127 }
1128
1129 #[test]
1130 fn test_suspend_until() {
1131 let error = DurableError::suspend_until(1234567890.0);
1132 assert!(error.is_suspend());
1133 if let DurableError::Suspend {
1134 scheduled_timestamp,
1135 } = error
1136 {
1137 assert_eq!(scheduled_timestamp, Some(1234567890.0));
1138 }
1139 }
1140
1141 #[test]
1142 fn test_error_object_from_durable_error() {
1143 let error = DurableError::validation("invalid input");
1144 let obj: ErrorObject = (&error).into();
1145 assert_eq!(obj.error_type, "ValidationError");
1146 assert_eq!(obj.error_message, "invalid input");
1147 }
1148
1149 #[test]
1150 fn test_from_serde_json_error() {
1151 let json_error = serde_json::from_str::<String>("invalid").unwrap_err();
1152 let error: DurableError = json_error.into();
1153 assert!(matches!(error, DurableError::SerDes { .. }));
1154 }
1155
1156 #[test]
1157 fn test_is_invalid_checkpoint_token_with_aws_error() {
1158 let error = DurableError::Checkpoint {
1159 message: "Checkpoint API returned 400: Invalid checkpoint token".to_string(),
1160 is_retriable: true,
1161 aws_error: Some(AwsError {
1162 code: "InvalidParameterValueException".to_string(),
1163 message: "Invalid checkpoint token: token has been consumed".to_string(),
1164 request_id: None,
1165 }),
1166 };
1167 assert!(error.is_invalid_checkpoint_token());
1168 assert!(error.is_retriable());
1169 }
1170
1171 #[test]
1172 fn test_is_invalid_checkpoint_token_without_aws_error() {
1173 let error = DurableError::Checkpoint {
1174 message: "Invalid checkpoint token: token expired".to_string(),
1175 is_retriable: true,
1176 aws_error: None,
1177 };
1178 assert!(error.is_invalid_checkpoint_token());
1179 }
1180
1181 #[test]
1182 fn test_is_not_invalid_checkpoint_token() {
1183 let error = DurableError::Checkpoint {
1184 message: "Network error".to_string(),
1185 is_retriable: true,
1186 aws_error: None,
1187 };
1188 assert!(!error.is_invalid_checkpoint_token());
1189 }
1190
1191 #[test]
1192 fn test_is_invalid_checkpoint_token_wrong_error_type() {
1193 let error = DurableError::Validation {
1194 message: "Invalid checkpoint token".to_string(),
1195 };
1196 assert!(!error.is_invalid_checkpoint_token());
1197 }
1198
1199 #[test]
1200 fn test_is_invalid_checkpoint_token_wrong_aws_error_code() {
1201 let error = DurableError::Checkpoint {
1202 message: "Some error".to_string(),
1203 is_retriable: false,
1204 aws_error: Some(AwsError {
1205 code: "ServiceException".to_string(),
1206 message: "Invalid checkpoint token".to_string(),
1207 request_id: None,
1208 }),
1209 };
1210 assert!(!error.is_invalid_checkpoint_token());
1212 }
1213
1214 #[test]
1215 fn test_size_limit_error() {
1216 let error = DurableError::size_limit("Payload too large");
1217 assert!(error.is_size_limit());
1218 assert!(!error.is_retriable());
1219 assert!(!error.is_throttling());
1220 assert!(!error.is_resource_not_found());
1221 }
1222
1223 #[test]
1224 fn test_size_limit_error_with_details() {
1225 let error =
1226 DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
1227 assert!(error.is_size_limit());
1228 if let DurableError::SizeLimit {
1229 actual_size,
1230 max_size,
1231 ..
1232 } = error
1233 {
1234 assert_eq!(actual_size, Some(7_000_000));
1235 assert_eq!(max_size, Some(6_000_000));
1236 } else {
1237 panic!("Expected SizeLimit error");
1238 }
1239 }
1240
1241 #[test]
1242 fn test_throttling_error() {
1243 let error = DurableError::throttling("Rate limit exceeded");
1244 assert!(error.is_throttling());
1245 assert!(!error.is_retriable());
1246 assert!(!error.is_size_limit());
1247 assert!(!error.is_resource_not_found());
1248 assert_eq!(error.get_retry_after_ms(), None);
1249 }
1250
1251 #[test]
1252 fn test_throttling_error_with_retry_delay() {
1253 let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
1254 assert!(error.is_throttling());
1255 assert_eq!(error.get_retry_after_ms(), Some(5000));
1256 }
1257
1258 #[test]
1259 fn test_resource_not_found_error() {
1260 let error = DurableError::resource_not_found("Execution not found");
1261 assert!(error.is_resource_not_found());
1262 assert!(!error.is_retriable());
1263 assert!(!error.is_size_limit());
1264 assert!(!error.is_throttling());
1265 }
1266
1267 #[test]
1268 fn test_resource_not_found_error_with_id() {
1269 let error = DurableError::resource_not_found_with_id(
1270 "Execution not found",
1271 "arn:aws:lambda:us-east-1:123456789012:function:test",
1272 );
1273 assert!(error.is_resource_not_found());
1274 if let DurableError::ResourceNotFound { resource_id, .. } = error {
1275 assert_eq!(
1276 resource_id,
1277 Some("arn:aws:lambda:us-east-1:123456789012:function:test".to_string())
1278 );
1279 } else {
1280 panic!("Expected ResourceNotFound error");
1281 }
1282 }
1283
1284 #[test]
1285 fn test_error_object_from_size_limit_error() {
1286 let error =
1287 DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
1288 let obj: ErrorObject = (&error).into();
1289 assert_eq!(obj.error_type, "SizeLimitExceededError");
1290 assert!(obj.error_message.contains("Payload too large"));
1291 assert!(obj.error_message.contains("7000000"));
1292 assert!(obj.error_message.contains("6000000"));
1293 }
1294
1295 #[test]
1296 fn test_error_object_from_throttling_error() {
1297 let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
1298 let obj: ErrorObject = (&error).into();
1299 assert_eq!(obj.error_type, "ThrottlingError");
1300 assert!(obj.error_message.contains("Rate limit exceeded"));
1301 assert!(obj.error_message.contains("5000ms"));
1302 }
1303
1304 #[test]
1305 fn test_error_object_from_resource_not_found_error() {
1306 let error = DurableError::resource_not_found_with_id("Execution not found", "test-arn");
1307 let obj: ErrorObject = (&error).into();
1308 assert_eq!(obj.error_type, "ResourceNotFoundError");
1309 assert!(obj.error_message.contains("Execution not found"));
1310 assert!(obj.error_message.contains("test-arn"));
1311 }
1312
1313 #[test]
1314 fn test_get_retry_after_ms_non_throttling() {
1315 let error = DurableError::validation("test");
1316 assert_eq!(error.get_retry_after_ms(), None);
1317 }
1318
1319 #[test]
1323 fn test_termination_reason_size_is_one_byte() {
1324 assert_eq!(
1325 std::mem::size_of::<TerminationReason>(),
1326 1,
1327 "TerminationReason should be 1 byte with #[repr(u8)]"
1328 );
1329 }
1330
1331 #[test]
1333 fn test_termination_reason_discriminant_values() {
1334 assert_eq!(TerminationReason::UnhandledError as u8, 0);
1336 assert_eq!(TerminationReason::InvocationError as u8, 1);
1337 assert_eq!(TerminationReason::ExecutionError as u8, 2);
1338 assert_eq!(TerminationReason::CheckpointFailed as u8, 3);
1339 assert_eq!(TerminationReason::NonDeterministicExecution as u8, 4);
1340 assert_eq!(TerminationReason::StepInterrupted as u8, 5);
1341 assert_eq!(TerminationReason::CallbackError as u8, 6);
1342 assert_eq!(TerminationReason::SerializationError as u8, 7);
1343 assert_eq!(TerminationReason::SizeLimitExceeded as u8, 8);
1344
1345 assert_eq!(TerminationReason::OperationTerminated as u8, 9);
1347 assert_eq!(TerminationReason::RetryScheduled as u8, 10);
1348 assert_eq!(TerminationReason::WaitScheduled as u8, 11);
1349 assert_eq!(TerminationReason::CallbackPending as u8, 12);
1350 assert_eq!(TerminationReason::ContextValidationError as u8, 13);
1351 assert_eq!(TerminationReason::LambdaTimeoutApproaching as u8, 14);
1352 }
1353
1354 #[test]
1358 fn test_termination_reason_serde_uses_string_representation() {
1359 let reason = TerminationReason::UnhandledError;
1361 let json = serde_json::to_string(&reason).unwrap();
1362 assert_eq!(json, "\"UnhandledError\"");
1363
1364 let reason = TerminationReason::InvocationError;
1365 let json = serde_json::to_string(&reason).unwrap();
1366 assert_eq!(json, "\"InvocationError\"");
1367
1368 let reason = TerminationReason::ExecutionError;
1369 let json = serde_json::to_string(&reason).unwrap();
1370 assert_eq!(json, "\"ExecutionError\"");
1371
1372 let reason = TerminationReason::CheckpointFailed;
1373 let json = serde_json::to_string(&reason).unwrap();
1374 assert_eq!(json, "\"CheckpointFailed\"");
1375
1376 let reason = TerminationReason::NonDeterministicExecution;
1377 let json = serde_json::to_string(&reason).unwrap();
1378 assert_eq!(json, "\"NonDeterministicExecution\"");
1379
1380 let reason = TerminationReason::StepInterrupted;
1381 let json = serde_json::to_string(&reason).unwrap();
1382 assert_eq!(json, "\"StepInterrupted\"");
1383
1384 let reason = TerminationReason::CallbackError;
1385 let json = serde_json::to_string(&reason).unwrap();
1386 assert_eq!(json, "\"CallbackError\"");
1387
1388 let reason = TerminationReason::SerializationError;
1389 let json = serde_json::to_string(&reason).unwrap();
1390 assert_eq!(json, "\"SerializationError\"");
1391
1392 let reason = TerminationReason::SizeLimitExceeded;
1393 let json = serde_json::to_string(&reason).unwrap();
1394 assert_eq!(json, "\"SizeLimitExceeded\"");
1395
1396 let reason = TerminationReason::OperationTerminated;
1397 let json = serde_json::to_string(&reason).unwrap();
1398 assert_eq!(json, "\"OperationTerminated\"");
1399
1400 let reason = TerminationReason::RetryScheduled;
1401 let json = serde_json::to_string(&reason).unwrap();
1402 assert_eq!(json, "\"RetryScheduled\"");
1403
1404 let reason = TerminationReason::WaitScheduled;
1405 let json = serde_json::to_string(&reason).unwrap();
1406 assert_eq!(json, "\"WaitScheduled\"");
1407
1408 let reason = TerminationReason::CallbackPending;
1409 let json = serde_json::to_string(&reason).unwrap();
1410 assert_eq!(json, "\"CallbackPending\"");
1411
1412 let reason = TerminationReason::ContextValidationError;
1413 let json = serde_json::to_string(&reason).unwrap();
1414 assert_eq!(json, "\"ContextValidationError\"");
1415
1416 let reason = TerminationReason::LambdaTimeoutApproaching;
1417 let json = serde_json::to_string(&reason).unwrap();
1418 assert_eq!(json, "\"LambdaTimeoutApproaching\"");
1419 }
1420
1421 #[test]
1422 fn test_termination_reason_serde_round_trip() {
1423 let reasons = [
1424 TerminationReason::UnhandledError,
1425 TerminationReason::InvocationError,
1426 TerminationReason::ExecutionError,
1427 TerminationReason::CheckpointFailed,
1428 TerminationReason::NonDeterministicExecution,
1429 TerminationReason::StepInterrupted,
1430 TerminationReason::CallbackError,
1431 TerminationReason::SerializationError,
1432 TerminationReason::SizeLimitExceeded,
1433 TerminationReason::OperationTerminated,
1434 TerminationReason::RetryScheduled,
1435 TerminationReason::WaitScheduled,
1436 TerminationReason::CallbackPending,
1437 TerminationReason::ContextValidationError,
1438 TerminationReason::LambdaTimeoutApproaching,
1439 ];
1440
1441 for reason in reasons {
1442 let json = serde_json::to_string(&reason).unwrap();
1443 let deserialized: TerminationReason = serde_json::from_str(&json).unwrap();
1444 assert_eq!(reason, deserialized, "Round-trip failed for {:?}", reason);
1445 }
1446 }
1447}