1use std::{sync::Arc, time::Duration};
8
9use anyhow::Result;
10use chrono::{DateTime, Utc};
11use tracing::{debug, info, instrument, warn};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum SignalType {
20 SignalCancel = 0,
22 SignalPause = 1,
24 SignalResume = 2,
26}
27
28impl From<SignalType> for i32 {
29 fn from(s: SignalType) -> i32 {
30 s as i32
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum InstanceEventType {
37 EventHeartbeat = 0,
39 EventCompleted = 2,
41 EventFailed = 3,
43 EventSuspended = 4,
45 EventCustom = 5,
47}
48
49impl From<InstanceEventType> for i32 {
50 fn from(e: InstanceEventType) -> i32 {
51 e as i32
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum InstanceStatus {
58 StatusUnknown = 0,
60 StatusPending = 1,
62 StatusRunning = 2,
64 StatusSuspended = 3,
66 StatusCompleted = 4,
68 StatusFailed = 5,
70 StatusCancelled = 6,
72}
73
74impl From<InstanceStatus> for i32 {
75 fn from(s: InstanceStatus) -> i32 {
76 s as i32
77 }
78}
79
80impl InstanceStatus {
81 pub fn try_from_i32(value: i32) -> Option<Self> {
83 match value {
84 0 => Some(Self::StatusUnknown),
85 1 => Some(Self::StatusPending),
86 2 => Some(Self::StatusRunning),
87 3 => Some(Self::StatusSuspended),
88 4 => Some(Self::StatusCompleted),
89 5 => Some(Self::StatusFailed),
90 6 => Some(Self::StatusCancelled),
91 _ => None,
92 }
93 }
94}
95
96pub struct RegisterInstanceRequest {
98 pub instance_id: String,
100 pub tenant_id: String,
102 pub checkpoint_id: Option<String>,
104}
105
106pub struct RegisterInstanceResponse {
108 pub success: bool,
110 pub error: String,
112}
113
114pub struct CheckpointRequest {
116 pub instance_id: String,
118 pub checkpoint_id: String,
120 pub state: Vec<u8>,
122}
123
124#[derive(Debug, Clone)]
126pub struct Signal {
127 pub instance_id: String,
129 pub signal_type: i32,
131 pub payload: Vec<u8>,
133}
134
135#[derive(Debug, Clone)]
137pub struct CustomSignal {
138 pub checkpoint_id: String,
140 pub payload: Vec<u8>,
142}
143
144#[derive(Debug, Clone)]
146pub struct CheckpointErrorInfo {
147 pub code: String,
149 pub message: String,
151}
152
153pub struct CheckpointResponse {
155 pub found: bool,
157 pub state: Vec<u8>,
159 pub pending_signal: Option<Signal>,
161 pub custom_signal: Option<CustomSignal>,
163 pub last_error: Option<CheckpointErrorInfo>,
165}
166
167pub struct GetCheckpointRequest {
169 pub instance_id: String,
171 pub checkpoint_id: String,
173}
174
175pub struct GetCheckpointResponse {
177 pub found: bool,
179 pub state: Vec<u8>,
181}
182
183pub struct SleepRequest {
185 pub instance_id: String,
187 pub duration_ms: u64,
189 pub checkpoint_id: String,
191 pub state: Vec<u8>,
193}
194
195pub struct SleepResponse {}
197
198pub struct InstanceEvent {
200 pub instance_id: String,
202 pub event_type: i32,
204 pub checkpoint_id: Option<String>,
206 pub payload: Vec<u8>,
208 pub timestamp_ms: i64,
210 pub subtype: Option<String>,
212}
213
214impl InstanceEvent {
215 pub fn event_type(&self) -> InstanceEventType {
217 match self.event_type {
218 0 => InstanceEventType::EventHeartbeat,
219 2 => InstanceEventType::EventCompleted,
220 3 => InstanceEventType::EventFailed,
221 4 => InstanceEventType::EventSuspended,
222 5 => InstanceEventType::EventCustom,
223 _ => InstanceEventType::EventCustom,
224 }
225 }
226}
227
228pub struct InstanceEventResponse {
230 pub success: bool,
232 pub error: Option<String>,
234}
235
236pub struct GetInstanceStatusRequest {
238 pub instance_id: String,
240}
241
242pub struct GetInstanceStatusResponse {
244 pub instance_id: String,
246 pub status: i32,
248 pub checkpoint_id: Option<String>,
250 pub started_at_ms: i64,
252 pub finished_at_ms: Option<i64>,
254 pub output: Option<Vec<u8>>,
256 pub error: Option<String>,
258}
259
260pub struct PollSignalsRequest {
262 pub instance_id: String,
264 pub checkpoint_id: Option<String>,
266}
267
268pub struct PollSignalsResponse {
270 pub signal: Option<Signal>,
272 pub custom_signal: Option<CustomSignal>,
274}
275
276pub struct SignalAck {
278 pub instance_id: String,
280 pub signal_type: i32,
282 pub acknowledged: bool,
284}
285
286impl SignalAck {
287 pub fn signal_type(&self) -> SignalType {
289 match self.signal_type {
290 0 => SignalType::SignalCancel,
291 1 => SignalType::SignalPause,
292 2 => SignalType::SignalResume,
293 _ => SignalType::SignalCancel,
294 }
295 }
296}
297
298pub struct ErrorMetadata {
300 pub category: i32,
302 pub severity: i32,
304 pub retry_hint: i32,
306 pub retry_after_ms: Option<u64>,
308 pub error_code: Option<String>,
310}
311
312impl ErrorMetadata {
313 pub fn category(&self) -> &'static str {
315 match self.category {
316 1 => "transient",
317 2 => "permanent",
318 3 => "business",
319 _ => "unknown",
320 }
321 }
322
323 pub fn severity(&self) -> &'static str {
325 match self.severity {
326 0 => "info",
327 1 => "warning",
328 2 => "error",
329 3 => "critical",
330 _ => "unknown",
331 }
332 }
333
334 pub fn retry_hint(&self) -> &'static str {
336 match self.retry_hint {
337 1 => "retry_immediately",
338 2 => "retry_with_backoff",
339 3 => "retry_after",
340 4 => "do_not_retry",
341 _ => "unknown",
342 }
343 }
344}
345
346pub struct RetryAttemptEvent {
348 pub instance_id: String,
350 pub checkpoint_id: String,
352 pub attempt_number: u32,
354 pub timestamp_ms: i64,
356 pub error_message: Option<String>,
358 pub error_metadata: Option<ErrorMetadata>,
360}
361
362use crate::error::CoreError;
363use crate::persistence::{EventRecord, Persistence};
364
365pub struct InstanceHandlerState {
369 pub persistence: Arc<dyn Persistence>,
371}
372
373impl InstanceHandlerState {
374 pub fn new(persistence: Arc<dyn Persistence>) -> Self {
376 Self { persistence }
377 }
378}
379
380#[instrument(skip(state, request), fields(instance_id = %request.instance_id))]
395pub async fn handle_register_instance(
396 state: &InstanceHandlerState,
397 request: RegisterInstanceRequest,
398) -> Result<RegisterInstanceResponse> {
399 info!(
400 tenant_id = %request.tenant_id,
401 resuming_from = ?request.checkpoint_id,
402 "Instance registering"
403 );
404
405 if request.instance_id.is_empty() {
407 return Ok(RegisterInstanceResponse {
408 success: false,
409 error: "instance_id is required".to_string(),
410 });
411 }
412
413 if request.tenant_id.is_empty() {
415 return Ok(RegisterInstanceResponse {
416 success: false,
417 error: "tenant_id is required".to_string(),
418 });
419 }
420
421 if let Some(ref cp_id) = request.checkpoint_id {
423 let checkpoint = state
424 .persistence
425 .load_checkpoint(&request.instance_id, cp_id)
426 .await;
427 match checkpoint {
428 Ok(Some(_)) => {
429 debug!(checkpoint_id = %cp_id, "Checkpoint found for resume");
430 }
431 Ok(None) => {
432 return Ok(RegisterInstanceResponse {
433 success: false,
434 error: format!("Checkpoint '{}' not found", cp_id),
435 });
436 }
437 Err(e) => {
438 return Ok(RegisterInstanceResponse {
439 success: false,
440 error: format!("Failed to verify checkpoint: {}", e),
441 });
442 }
443 }
444 }
445
446 let instance_exists = state
448 .persistence
449 .get_instance(&request.instance_id)
450 .await
451 .map(|opt| opt.is_some())
452 .unwrap_or(false);
453
454 if !instance_exists {
455 info!("Instance not found, creating self-registered instance");
457 if let Err(e) = state
458 .persistence
459 .register_instance(&request.instance_id, &request.tenant_id)
460 .await
461 {
462 return Ok(RegisterInstanceResponse {
463 success: false,
464 error: format!("Failed to create instance: {}", e),
465 });
466 }
467 }
468
469 let started_at = Utc::now();
471 if let Err(e) = state
472 .persistence
473 .update_instance_status(&request.instance_id, "running", Some(started_at))
474 .await
475 {
476 return Ok(RegisterInstanceResponse {
477 success: false,
478 error: format!("Failed to update instance status: {}", e),
479 });
480 }
481
482 let event = EventRecord {
484 id: None,
485 instance_id: request.instance_id.clone(),
486 event_type: "started".to_string(),
487 checkpoint_id: request.checkpoint_id.clone(),
488 payload: None,
489 created_at: started_at,
490 subtype: None,
491 };
492 if let Err(e) = state.persistence.insert_event(&event).await {
493 warn!("Failed to insert started event: {}", e);
494 }
496
497 info!("Instance registered successfully");
498
499 Ok(RegisterInstanceResponse {
500 success: true,
501 error: String::new(),
502 })
503}
504
505#[instrument(skip(state, request), fields(instance_id = %request.instance_id, checkpoint_id = %request.checkpoint_id))]
517pub async fn handle_checkpoint(
518 state: &InstanceHandlerState,
519 request: CheckpointRequest,
520) -> Result<CheckpointResponse> {
521 debug!(
522 state_size = request.state.len(),
523 "Processing checkpoint request"
524 );
525
526 let instance = state.persistence.get_instance(&request.instance_id).await?;
528 match instance {
529 Some(inst) => {
530 if inst.status != "running" {
531 return Err(CoreError::InvalidInstanceState {
532 instance_id: request.instance_id.clone(),
533 expected: "running".to_string(),
534 actual: inst.status,
535 }
536 .into());
537 }
538 }
539 None => {
540 return Err(CoreError::InstanceNotFound {
541 instance_id: request.instance_id.clone(),
542 }
543 .into());
544 }
545 }
546
547 if let Some(existing) = state
549 .persistence
550 .load_checkpoint(&request.instance_id, &request.checkpoint_id)
551 .await?
552 {
553 debug!(
554 checkpoint_id = %request.checkpoint_id,
555 state_size = existing.state.len(),
556 "Found existing checkpoint - returning for resume"
557 );
558
559 let pending_signal =
561 get_pending_signal(state.persistence.as_ref(), &request.instance_id).await;
562 let custom_signal = state
563 .persistence
564 .take_pending_custom_signal(&request.instance_id, &request.checkpoint_id)
565 .await?
566 .map(|sig| CustomSignal {
567 checkpoint_id: request.checkpoint_id.clone(),
568 payload: sig.payload.unwrap_or_default(),
569 });
570
571 return Ok(CheckpointResponse {
572 found: true,
573 state: existing.state,
574 pending_signal,
575 custom_signal,
576 last_error: None, });
578 }
579
580 if request.state.is_empty() {
586 return Ok(CheckpointResponse {
587 found: false,
588 state: vec![],
589 pending_signal: get_pending_signal(state.persistence.as_ref(), &request.instance_id)
590 .await,
591 custom_signal: None,
592 last_error: None,
593 });
594 }
595
596 state
597 .persistence
598 .save_checkpoint(&request.instance_id, &request.checkpoint_id, &request.state)
599 .await?;
600
601 state
603 .persistence
604 .update_instance_checkpoint(&request.instance_id, &request.checkpoint_id)
605 .await?;
606
607 let pending_signal = get_pending_signal(state.persistence.as_ref(), &request.instance_id).await;
609 let custom_signal = state
610 .persistence
611 .take_pending_custom_signal(&request.instance_id, &request.checkpoint_id)
612 .await?
613 .map(|sig| CustomSignal {
614 checkpoint_id: request.checkpoint_id.clone(),
615 payload: sig.payload.unwrap_or_default(),
616 });
617
618 if pending_signal.is_some() || custom_signal.is_some() {
619 debug!(
620 ?pending_signal,
621 has_custom = custom_signal.is_some(),
622 "Checkpoint saved with pending signal"
623 );
624 } else {
625 debug!("New checkpoint saved successfully");
626 }
627
628 Ok(CheckpointResponse {
629 found: false,
630 state: Vec::new(),
631 pending_signal,
632 custom_signal,
633 last_error: None,
634 })
635}
636
637async fn get_pending_signal(persistence: &dyn Persistence, instance_id: &str) -> Option<Signal> {
639 match persistence.get_pending_signal(instance_id).await {
640 Ok(Some(signal)) => {
641 let signal_type = match signal.signal_type.as_str() {
642 "cancel" => SignalType::SignalCancel,
643 "pause" => SignalType::SignalPause,
644 "resume" => SignalType::SignalResume,
645 _ => return None,
646 };
647 Some(Signal {
648 instance_id: instance_id.to_string(),
649 signal_type: signal_type.into(),
650 payload: signal.payload.unwrap_or_default(),
651 })
652 }
653 _ => None,
654 }
655}
656
657#[instrument(skip(state, request), fields(instance_id = %request.instance_id, checkpoint_id = %request.checkpoint_id))]
661pub async fn handle_get_checkpoint(
662 state: &InstanceHandlerState,
663 request: GetCheckpointRequest,
664) -> Result<GetCheckpointResponse> {
665 debug!("Looking up checkpoint (read-only)");
666
667 let instance = state.persistence.get_instance(&request.instance_id).await?;
669 if instance.is_none() {
670 return Err(CoreError::InstanceNotFound {
671 instance_id: request.instance_id.clone(),
672 }
673 .into());
674 }
675
676 if let Some(checkpoint) = state
678 .persistence
679 .load_checkpoint(&request.instance_id, &request.checkpoint_id)
680 .await?
681 {
682 debug!(
683 checkpoint_id = %request.checkpoint_id,
684 state_size = checkpoint.state.len(),
685 "Checkpoint found"
686 );
687 return Ok(GetCheckpointResponse {
688 found: true,
689 state: checkpoint.state,
690 });
691 }
692
693 debug!(checkpoint_id = %request.checkpoint_id, "Checkpoint not found");
694 Ok(GetCheckpointResponse {
695 found: false,
696 state: Vec::new(),
697 })
698}
699
700#[instrument(skip(state, request), fields(instance_id = %request.instance_id, checkpoint_id = %request.checkpoint_id))]
710pub async fn handle_sleep(
711 state: &InstanceHandlerState,
712 request: SleepRequest,
713) -> Result<SleepResponse> {
714 debug!(
715 duration_ms = request.duration_ms,
716 state_size = request.state.len(),
717 "Processing sleep request"
718 );
719
720 if !request.checkpoint_id.is_empty() {
722 state
723 .persistence
724 .save_checkpoint(&request.instance_id, &request.checkpoint_id, &request.state)
725 .await?;
726
727 state
729 .persistence
730 .update_instance_checkpoint(&request.instance_id, &request.checkpoint_id)
731 .await?;
732
733 debug!(checkpoint_id = %request.checkpoint_id, "Sleep checkpoint saved");
734 }
735
736 tokio::time::sleep(Duration::from_millis(request.duration_ms)).await;
738 Ok(SleepResponse {})
739}
740
741#[instrument(skip(state, event), fields(instance_id = %event.instance_id, event_type = ?event.event_type()))]
757pub async fn handle_instance_event(
758 state: &InstanceHandlerState,
759 event: InstanceEvent,
760) -> Result<InstanceEventResponse> {
761 debug!(
762 event_type = ?event.event_type,
763 checkpoint_id = ?event.checkpoint_id,
764 payload_size = event.payload.len(),
765 timestamp_ms = event.timestamp_ms,
766 "Received instance event"
767 );
768
769 let event_type = map_event_type(event.event_type());
771
772 if event.instance_id.is_empty() {
774 return Err(CoreError::ValidationError {
775 field: "instance_id".to_string(),
776 message: "instance_id is required".to_string(),
777 }
778 .into());
779 }
780
781 let created_at = DateTime::from_timestamp_millis(event.timestamp_ms).unwrap_or_else(Utc::now);
783
784 let event_record = EventRecord {
786 id: None,
787 instance_id: event.instance_id.clone(),
788 event_type: event_type.to_string(),
789 checkpoint_id: event.checkpoint_id.clone(),
790 payload: if event.payload.is_empty() {
791 None
792 } else {
793 Some(event.payload.clone())
794 },
795 created_at,
796 subtype: event.subtype.clone(),
797 };
798 state.persistence.insert_event(&event_record).await?;
799
800 match event.event_type() {
803 InstanceEventType::EventHeartbeat => {
804 debug!("Heartbeat received");
807 }
808 InstanceEventType::EventCompleted => {
809 let output = if event.payload.is_empty() {
810 None
811 } else {
812 Some(event.payload.as_slice())
813 };
814 let applied = state
818 .persistence
819 .complete_instance_if_running(
820 &event.instance_id,
821 "completed",
822 output,
823 None,
824 None,
825 None,
826 )
827 .await?;
828 if applied {
829 info!("Instance completed successfully");
830 } else {
831 warn!("Instance completion skipped (already in terminal state)");
832 }
833 }
834 InstanceEventType::EventFailed => {
835 let error = if event.payload.is_empty() {
836 "Unknown error"
837 } else {
838 std::str::from_utf8(&event.payload).unwrap_or("Unknown error (binary payload)")
839 };
840 let applied = state
843 .persistence
844 .complete_instance_if_running(
845 &event.instance_id,
846 "failed",
847 None,
848 Some(error),
849 None,
850 None,
851 )
852 .await?;
853 if applied {
854 warn!(error = %error, "Instance failed");
855 } else {
856 warn!(error = %error, "Instance failure event skipped (already in terminal state)");
857 }
858 }
859 InstanceEventType::EventSuspended => {
860 if let (false, Some(checkpoint_id)) = (event.payload.is_empty(), &event.checkpoint_id) {
862 if let Some(sleep_data) = parse_sleep_payload(&event.payload) {
863 state
865 .persistence
866 .save_checkpoint(&event.instance_id, checkpoint_id, &sleep_data.state)
867 .await?;
868
869 state
871 .persistence
872 .update_instance_checkpoint(&event.instance_id, checkpoint_id)
873 .await?;
874
875 if let Some(wake_at) = sleep_data.wake_at {
877 state
878 .persistence
879 .set_instance_sleep(&event.instance_id, wake_at)
880 .await?;
881 }
882
883 let applied = state
886 .persistence
887 .complete_instance_with_termination_if_running(
888 &event.instance_id,
889 "suspended",
890 Some("sleeping"),
891 None, None, None, None, Some(checkpoint_id),
896 )
897 .await?;
898
899 if applied {
900 info!(
901 checkpoint_id = %checkpoint_id,
902 wake_at = ?sleep_data.wake_at,
903 "Instance sleeping until scheduled wake"
904 );
905 } else {
906 warn!(
907 checkpoint_id = %checkpoint_id,
908 "Instance sleep event skipped (already in terminal state)"
909 );
910 }
911 } else {
912 let applied = state
915 .persistence
916 .complete_instance_if_running(
917 &event.instance_id,
918 "suspended",
919 None,
920 None,
921 None,
922 None,
923 )
924 .await?;
925 if applied {
926 info!("Instance suspended (with payload but no valid sleep data)");
927 } else {
928 warn!("Instance suspend event skipped (already in terminal state)");
929 }
930 }
931 } else {
932 let applied = state
935 .persistence
936 .complete_instance_if_running(
937 &event.instance_id,
938 "suspended",
939 None,
940 None,
941 None,
942 None,
943 )
944 .await?;
945 if applied {
946 info!("Instance suspended");
947 } else {
948 warn!("Instance suspend event skipped (already in terminal state)");
949 }
950 }
951 }
952 InstanceEventType::EventCustom => {
953 debug!(subtype = ?event.subtype, "Custom event received");
956 }
957 }
958
959 Ok(InstanceEventResponse {
960 success: true,
961 error: None,
962 })
963}
964
965#[instrument(skip(state, request), fields(instance_id = %request.instance_id))]
977pub async fn handle_get_instance_status(
978 state: &InstanceHandlerState,
979 request: GetInstanceStatusRequest,
980) -> Result<GetInstanceStatusResponse> {
981 debug!("Getting instance status");
982
983 let instance = state.persistence.get_instance(&request.instance_id).await?;
984
985 match instance {
986 Some(inst) => {
987 let status = map_status(&inst.status);
988
989 Ok(GetInstanceStatusResponse {
990 instance_id: request.instance_id,
991 status: status.into(),
992 checkpoint_id: inst.checkpoint_id,
993 started_at_ms: inst.started_at.map(|t| t.timestamp_millis()).unwrap_or(0),
994 finished_at_ms: inst.finished_at.map(|t| t.timestamp_millis()),
995 output: inst.output,
996 error: inst.error,
997 })
998 }
999 None => Ok(GetInstanceStatusResponse {
1000 instance_id: request.instance_id,
1001 status: InstanceStatus::StatusUnknown.into(),
1002 checkpoint_id: None,
1003 started_at_ms: 0,
1004 finished_at_ms: None,
1005 output: None,
1006 error: Some("Instance not found".to_string()),
1007 }),
1008 }
1009}
1010
1011#[instrument(skip(state, request), fields(instance_id = %request.instance_id))]
1023pub async fn handle_poll_signals(
1024 state: &InstanceHandlerState,
1025 request: PollSignalsRequest,
1026) -> Result<PollSignalsResponse> {
1027 debug!("Instance polling for signals");
1028
1029 let pending = state
1030 .persistence
1031 .get_pending_signal(&request.instance_id)
1032 .await?;
1033 let custom = if let Some(checkpoint_id) = request.checkpoint_id.as_deref() {
1034 state
1035 .persistence
1036 .take_pending_custom_signal(&request.instance_id, checkpoint_id)
1037 .await?
1038 } else {
1039 None
1040 };
1041
1042 let signal = pending.map(|sig| {
1043 let signal_type = match sig.signal_type.as_str() {
1044 "cancel" => SignalType::SignalCancel,
1045 "pause" => SignalType::SignalPause,
1046 "resume" => SignalType::SignalResume,
1047 _ => {
1048 warn!(signal_type = %sig.signal_type, "Unknown signal type");
1049 SignalType::SignalCancel
1050 }
1051 };
1052
1053 Signal {
1054 instance_id: request.instance_id.clone(),
1055 signal_type: signal_type.into(),
1056 payload: sig.payload.unwrap_or_default(),
1057 }
1058 });
1059
1060 let custom_signal = custom.map(|sig| CustomSignal {
1061 checkpoint_id: sig.checkpoint_id,
1062 payload: sig.payload.unwrap_or_default(),
1063 });
1064
1065 if signal.is_some() || custom_signal.is_some() {
1066 debug!(
1067 has_global = signal.is_some(),
1068 has_custom = custom_signal.is_some(),
1069 "Returning pending signals"
1070 );
1071 }
1072
1073 Ok(PollSignalsResponse {
1074 signal,
1075 custom_signal,
1076 })
1077}
1078
1079#[instrument(skip(state, ack), fields(instance_id = %ack.instance_id))]
1084pub async fn handle_signal_ack(state: &InstanceHandlerState, ack: SignalAck) -> Result<()> {
1085 debug!(
1086 signal_type = ?ack.signal_type,
1087 acknowledged = ack.acknowledged,
1088 "Received signal acknowledgement"
1089 );
1090
1091 if ack.acknowledged {
1092 state
1094 .persistence
1095 .acknowledge_signal(&ack.instance_id)
1096 .await?;
1097
1098 match ack.signal_type() {
1100 SignalType::SignalCancel => {
1101 state
1103 .persistence
1104 .complete_instance_extended(
1105 &ack.instance_id,
1106 "cancelled",
1107 None, None, None, None, )
1112 .await?;
1113 info!("Instance cancelled");
1114 }
1115 SignalType::SignalPause => {
1116 state
1118 .persistence
1119 .update_instance_status(&ack.instance_id, "suspended", None)
1120 .await?;
1121 info!("Instance paused/suspended");
1122 }
1123 SignalType::SignalResume => {
1124 debug!("Resume signal acknowledged");
1126 }
1127 }
1128 } else {
1129 warn!("Signal was not acknowledged by instance");
1130 }
1131
1132 Ok(())
1133}
1134
1135#[instrument(skip(state, event), fields(
1147 instance_id = %event.instance_id,
1148 checkpoint_id = %event.checkpoint_id,
1149 attempt = event.attempt_number,
1150 error_message = ?event.error_message,
1151))]
1152pub async fn handle_retry_attempt(
1153 state: &InstanceHandlerState,
1154 event: RetryAttemptEvent,
1155) -> Result<()> {
1156 debug!(timestamp_ms = event.timestamp_ms, "Recording retry attempt");
1157
1158 state
1160 .persistence
1161 .save_retry_attempt(
1162 &event.instance_id,
1163 &event.checkpoint_id,
1164 event.attempt_number as i32,
1165 event.error_message.as_deref(),
1166 )
1167 .await?;
1168
1169 if let Some(ref meta) = event.error_metadata {
1170 info!(
1171 error_category = ?meta.category(),
1172 error_severity = ?meta.severity(),
1173 retry_hint = ?meta.retry_hint(),
1174 error_code = ?meta.error_code,
1175 retry_after_ms = ?meta.retry_after_ms,
1176 "Retry attempt with error metadata"
1177 );
1178 }
1179
1180 debug!("Retry attempt recorded");
1181
1182 Ok(())
1183}
1184
1185pub fn map_event_type(event_type: InstanceEventType) -> &'static str {
1191 match event_type {
1192 InstanceEventType::EventHeartbeat => "heartbeat",
1193 InstanceEventType::EventCompleted => "completed",
1194 InstanceEventType::EventFailed => "failed",
1195 InstanceEventType::EventSuspended => "suspended",
1196 InstanceEventType::EventCustom => "custom",
1197 }
1198}
1199
1200pub fn map_status(status: &str) -> InstanceStatus {
1202 match status {
1203 "pending" => InstanceStatus::StatusPending,
1204 "running" => InstanceStatus::StatusRunning,
1205 "suspended" => InstanceStatus::StatusSuspended,
1206 "completed" => InstanceStatus::StatusCompleted,
1207 "failed" => InstanceStatus::StatusFailed,
1208 "cancelled" => InstanceStatus::StatusCancelled,
1209 _ => InstanceStatus::StatusUnknown,
1210 }
1211}
1212
1213pub fn map_signal_type(signal_type: SignalType) -> &'static str {
1215 match signal_type {
1216 SignalType::SignalCancel => "cancel",
1217 SignalType::SignalPause => "pause",
1218 SignalType::SignalResume => "resume",
1219 }
1220}
1221
1222struct SleepPayload {
1224 wake_at: Option<DateTime<Utc>>,
1225 state: Vec<u8>,
1226}
1227
1228fn parse_sleep_payload(payload: &[u8]) -> Option<SleepPayload> {
1234 use base64::Engine;
1235
1236 let json: serde_json::Value = serde_json::from_slice(payload).ok()?;
1238
1239 let wake_at_ms = json.get("wake_at_ms")?.as_i64()?;
1241 let wake_at = DateTime::from_timestamp_millis(wake_at_ms);
1242
1243 let state_b64 = json.get("state")?.as_str()?;
1245 let state = base64::engine::general_purpose::STANDARD
1246 .decode(state_b64)
1247 .ok()?;
1248
1249 Some(SleepPayload { wake_at, state })
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use crate::persistence::{
1256 CheckpointRecord, CustomSignalRecord, InstanceRecord, ListEventsFilter,
1257 ListStepSummariesFilter, SignalRecord, StepSummaryRecord,
1258 };
1259 use async_trait::async_trait;
1260 use std::collections::HashMap;
1261 use std::sync::Mutex;
1262
1263 struct MockPersistence {
1265 instances: Mutex<HashMap<String, InstanceRecord>>,
1266 checkpoints: Mutex<HashMap<(String, String), CheckpointRecord>>,
1267 signals: Mutex<HashMap<String, SignalRecord>>,
1268 events: Mutex<Vec<EventRecord>>,
1269 custom_signals: Mutex<HashMap<(String, String), CustomSignalRecord>>,
1270 fail_register: Mutex<bool>,
1271 fail_status_update: Mutex<bool>,
1272 }
1273
1274 impl MockPersistence {
1275 fn new() -> Self {
1276 Self {
1277 instances: Mutex::new(HashMap::new()),
1278 checkpoints: Mutex::new(HashMap::new()),
1279 signals: Mutex::new(HashMap::new()),
1280 events: Mutex::new(Vec::new()),
1281 custom_signals: Mutex::new(HashMap::new()),
1282 fail_register: Mutex::new(false),
1283 fail_status_update: Mutex::new(false),
1284 }
1285 }
1286
1287 fn with_instance(self, instance: InstanceRecord) -> Self {
1288 self.instances
1289 .lock()
1290 .unwrap()
1291 .insert(instance.instance_id.clone(), instance);
1292 self
1293 }
1294
1295 fn with_checkpoint(self, checkpoint: CheckpointRecord) -> Self {
1296 self.checkpoints.lock().unwrap().insert(
1297 (
1298 checkpoint.instance_id.clone(),
1299 checkpoint.checkpoint_id.clone(),
1300 ),
1301 checkpoint,
1302 );
1303 self
1304 }
1305
1306 fn with_signal(self, signal: SignalRecord) -> Self {
1307 self.signals
1308 .lock()
1309 .unwrap()
1310 .insert(signal.instance_id.clone(), signal);
1311 self
1312 }
1313
1314 fn with_custom_signal(self, signal: CustomSignalRecord) -> Self {
1315 self.custom_signals.lock().unwrap().insert(
1316 (signal.instance_id.clone(), signal.checkpoint_id.clone()),
1317 signal,
1318 );
1319 self
1320 }
1321
1322 #[allow(dead_code)]
1323 fn set_fail_register(&self) {
1324 *self.fail_register.lock().unwrap() = true;
1325 }
1326
1327 #[allow(dead_code)]
1328 fn set_fail_status_update(&self) {
1329 *self.fail_status_update.lock().unwrap() = true;
1330 }
1331
1332 fn get_events(&self) -> Vec<EventRecord> {
1333 self.events.lock().unwrap().clone()
1334 }
1335 }
1336
1337 fn make_instance(instance_id: &str, tenant_id: &str, status: &str) -> InstanceRecord {
1338 InstanceRecord {
1339 instance_id: instance_id.to_string(),
1340 tenant_id: tenant_id.to_string(),
1341 definition_version: 1,
1342 status: status.to_string(),
1343 checkpoint_id: None,
1344 attempt: 1,
1345 max_attempts: 3,
1346 created_at: Utc::now(),
1347 started_at: None,
1348 finished_at: None,
1349 input: None,
1350 output: None,
1351 error: None,
1352 sleep_until: None,
1353 termination_reason: None,
1354 exit_code: None,
1355 }
1356 }
1357
1358 fn make_checkpoint(instance_id: &str, checkpoint_id: &str, state: &[u8]) -> CheckpointRecord {
1359 CheckpointRecord {
1360 id: 1,
1361 instance_id: instance_id.to_string(),
1362 checkpoint_id: checkpoint_id.to_string(),
1363 state: state.to_vec(),
1364 created_at: Utc::now(),
1365 is_compensatable: false,
1366 compensation_step_id: None,
1367 compensation_data: None,
1368 compensation_state: None,
1369 compensation_order: 0,
1370 }
1371 }
1372
1373 fn make_signal(instance_id: &str, signal_type: &str) -> SignalRecord {
1374 SignalRecord {
1375 instance_id: instance_id.to_string(),
1376 signal_type: signal_type.to_string(),
1377 payload: None,
1378 created_at: Utc::now(),
1379 acknowledged_at: None,
1380 }
1381 }
1382
1383 #[async_trait]
1384 impl Persistence for MockPersistence {
1385 async fn register_instance(
1386 &self,
1387 instance_id: &str,
1388 tenant_id: &str,
1389 ) -> std::result::Result<(), CoreError> {
1390 if *self.fail_register.lock().unwrap() {
1391 return Err(CoreError::DatabaseError {
1392 operation: "register_instance".to_string(),
1393 details: "Mock register failure".to_string(),
1394 });
1395 }
1396 let instance = make_instance(instance_id, tenant_id, "pending");
1397 self.instances
1398 .lock()
1399 .unwrap()
1400 .insert(instance_id.to_string(), instance);
1401 Ok(())
1402 }
1403
1404 async fn get_instance(
1405 &self,
1406 instance_id: &str,
1407 ) -> std::result::Result<Option<InstanceRecord>, CoreError> {
1408 Ok(self.instances.lock().unwrap().get(instance_id).cloned())
1409 }
1410
1411 async fn update_instance_status(
1412 &self,
1413 instance_id: &str,
1414 status: &str,
1415 started_at: Option<DateTime<Utc>>,
1416 ) -> std::result::Result<(), CoreError> {
1417 if *self.fail_status_update.lock().unwrap() {
1418 return Err(CoreError::DatabaseError {
1419 operation: "update_instance_status".to_string(),
1420 details: "Mock status update failure".to_string(),
1421 });
1422 }
1423 if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1424 inst.status = status.to_string();
1425 inst.started_at = started_at;
1426 }
1427 Ok(())
1428 }
1429
1430 async fn update_instance_checkpoint(
1431 &self,
1432 instance_id: &str,
1433 checkpoint_id: &str,
1434 ) -> std::result::Result<(), CoreError> {
1435 if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1436 inst.checkpoint_id = Some(checkpoint_id.to_string());
1437 }
1438 Ok(())
1439 }
1440
1441 async fn complete_instance(
1442 &self,
1443 instance_id: &str,
1444 output: Option<&[u8]>,
1445 error: Option<&str>,
1446 ) -> std::result::Result<(), CoreError> {
1447 if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1448 inst.status = if error.is_some() {
1449 "failed".to_string()
1450 } else {
1451 "completed".to_string()
1452 };
1453 inst.output = output.map(|o| o.to_vec());
1454 inst.error = error.map(|e| e.to_string());
1455 inst.finished_at = Some(Utc::now());
1456 }
1457 Ok(())
1458 }
1459
1460 async fn complete_instance_if_running(
1461 &self,
1462 instance_id: &str,
1463 status: &str,
1464 output: Option<&[u8]>,
1465 error: Option<&str>,
1466 _stderr: Option<&str>,
1467 checkpoint_id: Option<&str>,
1468 ) -> std::result::Result<bool, CoreError> {
1469 let mut instances = self.instances.lock().unwrap();
1470 if let Some(inst) = instances.get_mut(instance_id)
1471 && inst.status == "running"
1472 {
1473 inst.status = status.to_string();
1474 inst.output = output.map(|o| o.to_vec());
1475 inst.error = error.map(|e| e.to_string());
1476 inst.checkpoint_id = checkpoint_id.map(|s| s.to_string());
1477 if status == "completed"
1478 || status == "failed"
1479 || status == "cancelled"
1480 || status == "suspended"
1481 {
1482 inst.finished_at = Some(Utc::now());
1483 }
1484 return Ok(true);
1485 }
1486 Ok(false)
1487 }
1488
1489 async fn complete_instance_with_termination(
1490 &self,
1491 instance_id: &str,
1492 status: &str,
1493 termination_reason: Option<&str>,
1494 exit_code: Option<i32>,
1495 output: Option<&[u8]>,
1496 error: Option<&str>,
1497 _stderr: Option<&str>,
1498 checkpoint_id: Option<&str>,
1499 ) -> std::result::Result<(), CoreError> {
1500 if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1501 inst.status = status.to_string();
1502 inst.termination_reason = termination_reason.map(|s| s.to_string());
1503 inst.exit_code = exit_code;
1504 inst.output = output.map(|o| o.to_vec());
1505 inst.error = error.map(|e| e.to_string());
1506 inst.checkpoint_id = checkpoint_id.map(|s| s.to_string());
1507 if status == "completed" || status == "failed" || status == "cancelled" {
1508 inst.finished_at = Some(Utc::now());
1509 }
1510 }
1511 Ok(())
1512 }
1513
1514 async fn complete_instance_with_termination_if_running(
1515 &self,
1516 instance_id: &str,
1517 status: &str,
1518 termination_reason: Option<&str>,
1519 exit_code: Option<i32>,
1520 output: Option<&[u8]>,
1521 error: Option<&str>,
1522 _stderr: Option<&str>,
1523 checkpoint_id: Option<&str>,
1524 ) -> std::result::Result<bool, CoreError> {
1525 let mut instances = self.instances.lock().unwrap();
1526 if let Some(inst) = instances.get_mut(instance_id)
1527 && inst.status == "running"
1528 {
1529 inst.status = status.to_string();
1530 inst.termination_reason = termination_reason.map(|s| s.to_string());
1531 inst.exit_code = exit_code;
1532 inst.output = output.map(|o| o.to_vec());
1533 inst.error = error.map(|e| e.to_string());
1534 inst.checkpoint_id = checkpoint_id.map(|s| s.to_string());
1535 if status == "completed" || status == "failed" || status == "cancelled" {
1536 inst.finished_at = Some(Utc::now());
1537 }
1538 return Ok(true);
1539 }
1540 Ok(false)
1541 }
1542
1543 async fn save_checkpoint(
1544 &self,
1545 instance_id: &str,
1546 checkpoint_id: &str,
1547 state: &[u8],
1548 ) -> std::result::Result<(), CoreError> {
1549 let cp = make_checkpoint(instance_id, checkpoint_id, state);
1550 self.checkpoints
1551 .lock()
1552 .unwrap()
1553 .insert((instance_id.to_string(), checkpoint_id.to_string()), cp);
1554 Ok(())
1555 }
1556
1557 async fn load_checkpoint(
1558 &self,
1559 instance_id: &str,
1560 checkpoint_id: &str,
1561 ) -> std::result::Result<Option<CheckpointRecord>, CoreError> {
1562 Ok(self
1563 .checkpoints
1564 .lock()
1565 .unwrap()
1566 .get(&(instance_id.to_string(), checkpoint_id.to_string()))
1567 .cloned())
1568 }
1569
1570 async fn list_checkpoints(
1571 &self,
1572 _instance_id: &str,
1573 _checkpoint_id: Option<&str>,
1574 _limit: i64,
1575 _offset: i64,
1576 _created_after: Option<DateTime<Utc>>,
1577 _created_before: Option<DateTime<Utc>>,
1578 ) -> std::result::Result<Vec<CheckpointRecord>, CoreError> {
1579 Ok(Vec::new())
1580 }
1581
1582 async fn count_checkpoints(
1583 &self,
1584 _instance_id: &str,
1585 _checkpoint_id: Option<&str>,
1586 _created_after: Option<DateTime<Utc>>,
1587 _created_before: Option<DateTime<Utc>>,
1588 ) -> std::result::Result<i64, CoreError> {
1589 Ok(0)
1590 }
1591
1592 async fn insert_event(&self, event: &EventRecord) -> std::result::Result<(), CoreError> {
1593 self.events.lock().unwrap().push(event.clone());
1594 Ok(())
1595 }
1596
1597 async fn insert_signal(
1598 &self,
1599 instance_id: &str,
1600 signal_type: &str,
1601 _payload: &[u8],
1602 ) -> std::result::Result<(), CoreError> {
1603 let signal = make_signal(instance_id, signal_type);
1604 self.signals
1605 .lock()
1606 .unwrap()
1607 .insert(instance_id.to_string(), signal);
1608 Ok(())
1609 }
1610
1611 async fn get_pending_signal(
1612 &self,
1613 instance_id: &str,
1614 ) -> std::result::Result<Option<SignalRecord>, CoreError> {
1615 Ok(self.signals.lock().unwrap().get(instance_id).cloned())
1616 }
1617
1618 async fn acknowledge_signal(
1619 &self,
1620 instance_id: &str,
1621 ) -> std::result::Result<(), CoreError> {
1622 self.signals.lock().unwrap().remove(instance_id);
1623 Ok(())
1624 }
1625
1626 async fn insert_custom_signal(
1627 &self,
1628 _instance_id: &str,
1629 _checkpoint_id: &str,
1630 _payload: &[u8],
1631 ) -> std::result::Result<(), CoreError> {
1632 Ok(())
1633 }
1634
1635 async fn take_pending_custom_signal(
1636 &self,
1637 instance_id: &str,
1638 checkpoint_id: &str,
1639 ) -> std::result::Result<Option<CustomSignalRecord>, CoreError> {
1640 Ok(self
1641 .custom_signals
1642 .lock()
1643 .unwrap()
1644 .remove(&(instance_id.to_string(), checkpoint_id.to_string())))
1645 }
1646
1647 async fn save_retry_attempt(
1648 &self,
1649 _instance_id: &str,
1650 _checkpoint_id: &str,
1651 _attempt: i32,
1652 _error_message: Option<&str>,
1653 ) -> std::result::Result<(), CoreError> {
1654 Ok(())
1655 }
1656
1657 async fn list_instances(
1658 &self,
1659 _tenant_id: Option<&str>,
1660 _status: Option<&str>,
1661 _limit: i64,
1662 _offset: i64,
1663 ) -> std::result::Result<Vec<InstanceRecord>, CoreError> {
1664 Ok(Vec::new())
1665 }
1666
1667 async fn health_check_db(&self) -> std::result::Result<bool, CoreError> {
1668 Ok(true)
1669 }
1670
1671 async fn count_active_instances(&self) -> std::result::Result<i64, CoreError> {
1672 Ok(0)
1673 }
1674
1675 async fn set_instance_sleep(
1676 &self,
1677 instance_id: &str,
1678 sleep_until: DateTime<Utc>,
1679 ) -> std::result::Result<(), CoreError> {
1680 if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1681 inst.sleep_until = Some(sleep_until);
1682 }
1683 Ok(())
1684 }
1685
1686 async fn clear_instance_sleep(
1687 &self,
1688 _instance_id: &str,
1689 ) -> std::result::Result<(), CoreError> {
1690 Ok(())
1691 }
1692
1693 async fn get_sleeping_instances_due(
1694 &self,
1695 _limit: i64,
1696 ) -> std::result::Result<Vec<InstanceRecord>, CoreError> {
1697 Ok(Vec::new())
1698 }
1699
1700 async fn list_events(
1701 &self,
1702 _instance_id: &str,
1703 _filter: &ListEventsFilter,
1704 _limit: i64,
1705 _offset: i64,
1706 ) -> std::result::Result<Vec<EventRecord>, CoreError> {
1707 Ok(Vec::new())
1708 }
1709
1710 async fn count_events(
1711 &self,
1712 _instance_id: &str,
1713 _filter: &ListEventsFilter,
1714 ) -> std::result::Result<i64, CoreError> {
1715 Ok(0)
1716 }
1717
1718 async fn list_step_summaries(
1719 &self,
1720 _instance_id: &str,
1721 _filter: &ListStepSummariesFilter,
1722 _limit: i64,
1723 _offset: i64,
1724 ) -> std::result::Result<Vec<StepSummaryRecord>, CoreError> {
1725 Ok(Vec::new())
1726 }
1727
1728 async fn count_step_summaries(
1729 &self,
1730 _instance_id: &str,
1731 _filter: &ListStepSummariesFilter,
1732 ) -> std::result::Result<i64, CoreError> {
1733 Ok(0)
1734 }
1735 }
1736
1737 #[test]
1738 fn test_event_type_mapping() {
1739 assert_eq!(
1740 map_event_type(InstanceEventType::EventHeartbeat),
1741 "heartbeat"
1742 );
1743 assert_eq!(
1744 map_event_type(InstanceEventType::EventCompleted),
1745 "completed"
1746 );
1747 assert_eq!(map_event_type(InstanceEventType::EventFailed), "failed");
1748 assert_eq!(
1749 map_event_type(InstanceEventType::EventSuspended),
1750 "suspended"
1751 );
1752 assert_eq!(map_event_type(InstanceEventType::EventCustom), "custom");
1753 }
1754
1755 #[test]
1756 fn test_status_mapping_all_variants() {
1757 assert_eq!(map_status("pending"), InstanceStatus::StatusPending);
1758 assert_eq!(map_status("running"), InstanceStatus::StatusRunning);
1759 assert_eq!(map_status("suspended"), InstanceStatus::StatusSuspended);
1760 assert_eq!(map_status("completed"), InstanceStatus::StatusCompleted);
1761 assert_eq!(map_status("failed"), InstanceStatus::StatusFailed);
1762 assert_eq!(map_status("cancelled"), InstanceStatus::StatusCancelled);
1763 assert_eq!(map_status("invalid"), InstanceStatus::StatusUnknown);
1764 assert_eq!(map_status(""), InstanceStatus::StatusUnknown);
1765 }
1766
1767 #[test]
1768 fn test_signal_type_mapping() {
1769 assert_eq!(map_signal_type(SignalType::SignalCancel), "cancel");
1770 assert_eq!(map_signal_type(SignalType::SignalPause), "pause");
1771 assert_eq!(map_signal_type(SignalType::SignalResume), "resume");
1772 }
1773
1774 #[test]
1775 fn test_instance_handler_state_new() {
1776 let persistence = Arc::new(MockPersistence::new());
1777 let state = InstanceHandlerState::new(persistence);
1778 let _ = &state.persistence;
1780 }
1781
1782 #[tokio::test]
1787 async fn test_register_empty_instance_id() {
1788 let persistence = Arc::new(MockPersistence::new());
1789 let state = InstanceHandlerState::new(persistence);
1790
1791 let request = RegisterInstanceRequest {
1792 instance_id: "".to_string(),
1793 tenant_id: "tenant-1".to_string(),
1794 checkpoint_id: None,
1795 };
1796
1797 let result = handle_register_instance(&state, request).await.unwrap();
1798 assert!(!result.success);
1799 assert!(result.error.contains("instance_id is required"));
1800 }
1801
1802 #[tokio::test]
1803 async fn test_register_empty_tenant_id() {
1804 let persistence = Arc::new(MockPersistence::new());
1805 let state = InstanceHandlerState::new(persistence);
1806
1807 let request = RegisterInstanceRequest {
1808 instance_id: "inst-1".to_string(),
1809 tenant_id: "".to_string(),
1810 checkpoint_id: None,
1811 };
1812
1813 let result = handle_register_instance(&state, request).await.unwrap();
1814 assert!(!result.success);
1815 assert!(result.error.contains("tenant_id is required"));
1816 }
1817
1818 #[tokio::test]
1819 async fn test_register_self_registration() {
1820 let persistence = Arc::new(MockPersistence::new());
1821 let state = InstanceHandlerState::new(persistence);
1822
1823 let request = RegisterInstanceRequest {
1824 instance_id: "inst-new".to_string(),
1825 tenant_id: "tenant-1".to_string(),
1826 checkpoint_id: None,
1827 };
1828
1829 let result = handle_register_instance(&state, request).await.unwrap();
1830 assert!(result.success);
1831 assert!(result.error.is_empty());
1832 }
1833
1834 #[tokio::test]
1835 async fn test_register_existing_instance() {
1836 let persistence = Arc::new(
1837 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "pending")),
1838 );
1839 let state = InstanceHandlerState::new(persistence);
1840
1841 let request = RegisterInstanceRequest {
1842 instance_id: "inst-1".to_string(),
1843 tenant_id: "tenant-1".to_string(),
1844 checkpoint_id: None,
1845 };
1846
1847 let result = handle_register_instance(&state, request).await.unwrap();
1848 assert!(result.success);
1849 }
1850
1851 #[tokio::test]
1852 async fn test_register_with_valid_checkpoint() {
1853 let persistence = Arc::new(
1854 MockPersistence::new()
1855 .with_instance(make_instance("inst-1", "tenant-1", "pending"))
1856 .with_checkpoint(make_checkpoint("inst-1", "cp-1", b"state")),
1857 );
1858 let state = InstanceHandlerState::new(persistence);
1859
1860 let request = RegisterInstanceRequest {
1861 instance_id: "inst-1".to_string(),
1862 tenant_id: "tenant-1".to_string(),
1863 checkpoint_id: Some("cp-1".to_string()),
1864 };
1865
1866 let result = handle_register_instance(&state, request).await.unwrap();
1867 assert!(result.success);
1868 }
1869
1870 #[tokio::test]
1871 async fn test_register_with_invalid_checkpoint() {
1872 let persistence = Arc::new(
1873 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "pending")),
1874 );
1875 let state = InstanceHandlerState::new(persistence);
1876
1877 let request = RegisterInstanceRequest {
1878 instance_id: "inst-1".to_string(),
1879 tenant_id: "tenant-1".to_string(),
1880 checkpoint_id: Some("nonexistent".to_string()),
1881 };
1882
1883 let result = handle_register_instance(&state, request).await.unwrap();
1884 assert!(!result.success);
1885 assert!(result.error.contains("not found"));
1886 }
1887
1888 #[tokio::test]
1889 async fn test_register_creates_started_event() {
1890 let mock = MockPersistence::new();
1891 let persistence = Arc::new(mock);
1892 let state = InstanceHandlerState::new(persistence.clone());
1893
1894 let request = RegisterInstanceRequest {
1895 instance_id: "inst-1".to_string(),
1896 tenant_id: "tenant-1".to_string(),
1897 checkpoint_id: None,
1898 };
1899
1900 let result = handle_register_instance(&state, request).await.unwrap();
1901 assert!(result.success);
1902
1903 let events = persistence.get_events();
1905 assert!(!events.is_empty());
1906 assert_eq!(events[0].event_type, "started");
1907 assert_eq!(events[0].instance_id, "inst-1");
1908 }
1909
1910 #[tokio::test]
1915 async fn test_checkpoint_instance_not_found() {
1916 let persistence = Arc::new(MockPersistence::new());
1917 let state = InstanceHandlerState::new(persistence);
1918
1919 let request = CheckpointRequest {
1920 instance_id: "nonexistent".to_string(),
1921 checkpoint_id: "cp-1".to_string(),
1922 state: b"test state".to_vec(),
1923 };
1924
1925 let result = handle_checkpoint(&state, request).await;
1926 assert!(result.is_err());
1927 }
1928
1929 #[tokio::test]
1930 async fn test_checkpoint_instance_not_running() {
1931 let persistence = Arc::new(MockPersistence::new().with_instance(make_instance(
1932 "inst-1",
1933 "tenant-1",
1934 "completed",
1935 )));
1936 let state = InstanceHandlerState::new(persistence);
1937
1938 let request = CheckpointRequest {
1939 instance_id: "inst-1".to_string(),
1940 checkpoint_id: "cp-1".to_string(),
1941 state: b"test state".to_vec(),
1942 };
1943
1944 let result = handle_checkpoint(&state, request).await;
1945 assert!(result.is_err());
1946 }
1947
1948 #[tokio::test]
1949 async fn test_checkpoint_new_saves_state() {
1950 let persistence = Arc::new(
1951 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
1952 );
1953 let state = InstanceHandlerState::new(persistence);
1954
1955 let request = CheckpointRequest {
1956 instance_id: "inst-1".to_string(),
1957 checkpoint_id: "cp-1".to_string(),
1958 state: b"test state".to_vec(),
1959 };
1960
1961 let result = handle_checkpoint(&state, request).await.unwrap();
1962 assert!(!result.found); }
1964
1965 #[tokio::test]
1966 async fn test_checkpoint_existing_returns_state() {
1967 let persistence = Arc::new(
1968 MockPersistence::new()
1969 .with_instance(make_instance("inst-1", "tenant-1", "running"))
1970 .with_checkpoint(make_checkpoint("inst-1", "cp-1", b"existing state")),
1971 );
1972 let state = InstanceHandlerState::new(persistence);
1973
1974 let request = CheckpointRequest {
1975 instance_id: "inst-1".to_string(),
1976 checkpoint_id: "cp-1".to_string(),
1977 state: b"new state".to_vec(), };
1979
1980 let result = handle_checkpoint(&state, request).await.unwrap();
1981 assert!(result.found);
1982 assert_eq!(result.state, b"existing state");
1983 }
1984
1985 #[tokio::test]
1986 async fn test_checkpoint_returns_pending_signal() {
1987 let persistence = Arc::new(
1988 MockPersistence::new()
1989 .with_instance(make_instance("inst-1", "tenant-1", "running"))
1990 .with_signal(make_signal("inst-1", "cancel")),
1991 );
1992 let state = InstanceHandlerState::new(persistence);
1993
1994 let request = CheckpointRequest {
1995 instance_id: "inst-1".to_string(),
1996 checkpoint_id: "cp-1".to_string(),
1997 state: b"test state".to_vec(),
1998 };
1999
2000 let result = handle_checkpoint(&state, request).await.unwrap();
2001 assert!(result.pending_signal.is_some());
2002 let signal = result.pending_signal.unwrap();
2003 assert_eq!(signal.signal_type, SignalType::SignalCancel as i32);
2004 }
2005
2006 #[tokio::test]
2007 async fn test_checkpoint_returns_custom_signal() {
2008 let custom_signal = CustomSignalRecord {
2009 instance_id: "inst-1".to_string(),
2010 checkpoint_id: "cp-1".to_string(),
2011 payload: Some(b"custom payload".to_vec()),
2012 created_at: Utc::now(),
2013 };
2014 let persistence = Arc::new(
2015 MockPersistence::new()
2016 .with_instance(make_instance("inst-1", "tenant-1", "running"))
2017 .with_custom_signal(custom_signal),
2018 );
2019 let state = InstanceHandlerState::new(persistence);
2020
2021 let request = CheckpointRequest {
2022 instance_id: "inst-1".to_string(),
2023 checkpoint_id: "cp-1".to_string(),
2024 state: b"test state".to_vec(),
2025 };
2026
2027 let result = handle_checkpoint(&state, request).await.unwrap();
2028 assert!(result.custom_signal.is_some());
2029 let cs = result.custom_signal.unwrap();
2030 assert_eq!(cs.checkpoint_id, "cp-1");
2031 assert_eq!(cs.payload, b"custom payload");
2032 }
2033
2034 #[tokio::test]
2039 async fn test_get_status_not_found() {
2040 let persistence = Arc::new(MockPersistence::new());
2041 let state = InstanceHandlerState::new(persistence);
2042
2043 let request = GetInstanceStatusRequest {
2044 instance_id: "nonexistent".to_string(),
2045 };
2046
2047 let result = handle_get_instance_status(&state, request).await.unwrap();
2048 assert_eq!(result.status, InstanceStatus::StatusUnknown as i32);
2050 }
2051
2052 #[tokio::test]
2053 async fn test_get_status_found() {
2054 let persistence = Arc::new(
2055 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2056 );
2057 let state = InstanceHandlerState::new(persistence);
2058
2059 let request = GetInstanceStatusRequest {
2060 instance_id: "inst-1".to_string(),
2061 };
2062
2063 let result = handle_get_instance_status(&state, request).await.unwrap();
2064 assert_eq!(result.status, InstanceStatus::StatusRunning as i32);
2065 }
2066
2067 #[tokio::test]
2072 async fn test_poll_signals_no_signal() {
2073 let persistence = Arc::new(
2074 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2075 );
2076 let state = InstanceHandlerState::new(persistence);
2077
2078 let request = PollSignalsRequest {
2079 instance_id: "inst-1".to_string(),
2080 checkpoint_id: None,
2081 };
2082
2083 let result = handle_poll_signals(&state, request).await.unwrap();
2084 assert!(result.signal.is_none());
2085 }
2086
2087 #[tokio::test]
2088 async fn test_poll_signals_with_pending_signal() {
2089 let persistence = Arc::new(
2090 MockPersistence::new()
2091 .with_instance(make_instance("inst-1", "tenant-1", "running"))
2092 .with_signal(make_signal("inst-1", "pause")),
2093 );
2094 let state = InstanceHandlerState::new(persistence);
2095
2096 let request = PollSignalsRequest {
2097 instance_id: "inst-1".to_string(),
2098 checkpoint_id: None,
2099 };
2100
2101 let result = handle_poll_signals(&state, request).await.unwrap();
2102 assert!(result.signal.is_some());
2103 let signal = result.signal.unwrap();
2104 assert_eq!(signal.signal_type, SignalType::SignalPause as i32);
2105 }
2106
2107 #[tokio::test]
2112 async fn test_signal_ack_success() {
2113 let persistence = Arc::new(
2114 MockPersistence::new()
2115 .with_instance(make_instance("inst-1", "tenant-1", "running"))
2116 .with_signal(make_signal("inst-1", "cancel")),
2117 );
2118 let state = InstanceHandlerState::new(persistence.clone());
2119
2120 let request = SignalAck {
2121 instance_id: "inst-1".to_string(),
2122 signal_type: SignalType::SignalCancel as i32,
2123 acknowledged: true,
2124 };
2125
2126 handle_signal_ack(&state, request).await.unwrap();
2128
2129 assert!(
2131 persistence
2132 .get_pending_signal("inst-1")
2133 .await
2134 .unwrap()
2135 .is_none()
2136 );
2137 }
2138
2139 #[tokio::test]
2144 async fn test_handle_event_heartbeat() {
2145 let persistence = Arc::new(
2146 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2147 );
2148 let state = InstanceHandlerState::new(persistence.clone());
2149
2150 let event = InstanceEvent {
2151 instance_id: "inst-1".to_string(),
2152 event_type: InstanceEventType::EventHeartbeat as i32,
2153 checkpoint_id: None,
2154 payload: Vec::new(),
2155 timestamp_ms: chrono::Utc::now().timestamp_millis(),
2156 subtype: None,
2157 };
2158
2159 let result = handle_instance_event(&state, event).await.unwrap();
2160 assert!(result.success);
2161
2162 let events = persistence.get_events();
2164 assert!(!events.is_empty());
2165 assert_eq!(events[0].event_type, "heartbeat");
2166 }
2167
2168 #[tokio::test]
2169 async fn test_handle_event_completed() {
2170 let persistence = Arc::new(
2171 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2172 );
2173 let state = InstanceHandlerState::new(persistence.clone());
2174
2175 let event = InstanceEvent {
2176 instance_id: "inst-1".to_string(),
2177 event_type: InstanceEventType::EventCompleted as i32,
2178 checkpoint_id: None,
2179 payload: b"result".to_vec(),
2180 timestamp_ms: chrono::Utc::now().timestamp_millis(),
2181 subtype: None,
2182 };
2183
2184 let result = handle_instance_event(&state, event).await.unwrap();
2185 assert!(result.success);
2186
2187 let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2189 assert_eq!(inst.status, "completed");
2190 }
2191
2192 #[tokio::test]
2193 async fn test_handle_event_failed() {
2194 let persistence = Arc::new(
2195 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2196 );
2197 let state = InstanceHandlerState::new(persistence.clone());
2198
2199 let event = InstanceEvent {
2200 instance_id: "inst-1".to_string(),
2201 event_type: InstanceEventType::EventFailed as i32,
2202 checkpoint_id: None,
2203 payload: b"error message".to_vec(),
2204 timestamp_ms: chrono::Utc::now().timestamp_millis(),
2205 subtype: None,
2206 };
2207
2208 let result = handle_instance_event(&state, event).await.unwrap();
2209 assert!(result.success);
2210
2211 let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2213 assert_eq!(inst.status, "failed");
2214 }
2215
2216 #[tokio::test]
2217 async fn test_handle_event_suspended() {
2218 let persistence = Arc::new(
2219 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2220 );
2221 let state = InstanceHandlerState::new(persistence.clone());
2222
2223 let event = InstanceEvent {
2224 instance_id: "inst-1".to_string(),
2225 event_type: InstanceEventType::EventSuspended as i32,
2226 checkpoint_id: None,
2227 payload: Vec::new(),
2228 timestamp_ms: chrono::Utc::now().timestamp_millis(),
2229 subtype: None,
2230 };
2231
2232 let result = handle_instance_event(&state, event).await.unwrap();
2233 assert!(result.success);
2234
2235 let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2237 assert_eq!(inst.status, "suspended");
2238 }
2239
2240 #[tokio::test]
2241 async fn test_handle_event_custom() {
2242 let persistence = Arc::new(
2243 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2244 );
2245 let state = InstanceHandlerState::new(persistence.clone());
2246
2247 let event = InstanceEvent {
2248 instance_id: "inst-1".to_string(),
2249 event_type: InstanceEventType::EventCustom as i32,
2250 checkpoint_id: None,
2251 payload: b"custom data".to_vec(),
2252 timestamp_ms: chrono::Utc::now().timestamp_millis(),
2253 subtype: Some("my_custom_type".to_string()),
2254 };
2255
2256 let result = handle_instance_event(&state, event).await.unwrap();
2257 assert!(result.success);
2258
2259 let events = persistence.get_events();
2261 assert!(!events.is_empty());
2262 assert_eq!(events[0].event_type, "custom");
2263 assert_eq!(events[0].subtype.as_deref(), Some("my_custom_type"));
2264 }
2265
2266 #[tokio::test]
2267 async fn test_handle_event_suspended_with_sleep() {
2268 use base64::Engine;
2269
2270 let persistence = Arc::new(
2271 MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2272 );
2273 let state = InstanceHandlerState::new(persistence.clone());
2274
2275 let wake_at = chrono::Utc::now() + chrono::Duration::hours(1);
2277 let checkpoint_state = b"test checkpoint state";
2278 let payload = serde_json::json!({
2279 "wake_at_ms": wake_at.timestamp_millis(),
2280 "state": base64::engine::general_purpose::STANDARD.encode(checkpoint_state),
2281 });
2282
2283 let event = InstanceEvent {
2284 instance_id: "inst-1".to_string(),
2285 event_type: InstanceEventType::EventSuspended as i32,
2286 checkpoint_id: Some("sleep-cp-1".to_string()),
2287 payload: payload.to_string().into_bytes(),
2288 timestamp_ms: chrono::Utc::now().timestamp_millis(),
2289 subtype: None,
2290 };
2291
2292 let result = handle_instance_event(&state, event).await.unwrap();
2293 assert!(result.success);
2294
2295 let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2297 assert_eq!(inst.status, "suspended");
2298 assert_eq!(inst.termination_reason.as_deref(), Some("sleeping"));
2299 assert_eq!(inst.checkpoint_id.as_deref(), Some("sleep-cp-1"));
2300 assert!(inst.sleep_until.is_some());
2301
2302 let cp = persistence
2304 .load_checkpoint("inst-1", "sleep-cp-1")
2305 .await
2306 .unwrap()
2307 .unwrap();
2308 assert_eq!(cp.state, checkpoint_state);
2309 }
2310
2311 #[test]
2312 fn test_parse_sleep_payload_valid() {
2313 use base64::Engine;
2314
2315 let state = b"test state";
2316 let wake_at_ms = 1750000000000i64; let payload = serde_json::json!({
2318 "wake_at_ms": wake_at_ms,
2319 "state": base64::engine::general_purpose::STANDARD.encode(state),
2320 });
2321
2322 let result = parse_sleep_payload(&payload.to_string().into_bytes());
2323 assert!(result.is_some());
2324
2325 let sleep_data = result.unwrap();
2326 assert_eq!(sleep_data.state, state);
2327 assert!(sleep_data.wake_at.is_some());
2328 assert_eq!(sleep_data.wake_at.unwrap().timestamp_millis(), wake_at_ms);
2329 }
2330
2331 #[test]
2332 fn test_parse_sleep_payload_invalid_json() {
2333 let result = parse_sleep_payload(b"not json");
2334 assert!(result.is_none());
2335 }
2336
2337 #[test]
2338 fn test_parse_sleep_payload_missing_fields() {
2339 let payload = serde_json::json!({
2340 "wake_at_ms": 1750000000000i64,
2341 });
2343 let result = parse_sleep_payload(&payload.to_string().into_bytes());
2344 assert!(result.is_none());
2345 }
2346}