1use std::collections::HashMap;
2use std::sync::Arc;
3
4use chrono::Utc;
5use serde_json::{json, Value};
6use tracing::{debug, warn};
7
8use fakecloud_aws::arn::Arn;
9use fakecloud_core::delivery::DeliveryBus;
10use fakecloud_dynamodb::SharedDynamoDbState;
11
12use crate::choice::evaluate_choice;
13use crate::error_handling::{find_catcher, should_retry};
14use crate::io_processing::{apply_input_path, apply_output_path, apply_result_path};
15use crate::service::SharedServiceRegistry;
16use crate::state::{ExecutionStatus, HistoryEvent, MapRun, SharedStepFunctionsState};
17
18#[allow(clippy::too_many_arguments)]
21pub async fn execute_state_machine(
22 state: SharedStepFunctionsState,
23 execution_arn: String,
24 definition: String,
25 input: Option<String>,
26 delivery: Option<Arc<DeliveryBus>>,
27 dynamodb_state: Option<SharedDynamoDbState>,
28 registry: Option<SharedServiceRegistry>,
29 logging_configuration: Option<Value>,
30) {
31 let def: Value = match serde_json::from_str(&definition) {
32 Ok(v) => v,
33 Err(e) => {
34 fail_execution(
35 &state,
36 &execution_arn,
37 "States.Runtime",
38 &format!("Failed to parse definition: {e}"),
39 );
40 return;
41 }
42 };
43
44 let raw_input: Value = input
45 .as_deref()
46 .and_then(|s| serde_json::from_str(s).ok())
47 .unwrap_or(json!({}));
48
49 add_event(
51 &state,
52 &execution_arn,
53 "ExecutionStarted",
54 0,
55 json!({
56 "input": serde_json::to_string(&raw_input).expect("serde_json::Value serialization is infallible"),
57 "roleArn": "arn:aws:iam::123456789012:role/execution-role"
58 }),
59 );
60
61 let def_owned = def;
67 let state_clone = state.clone();
68 let execution_arn_clone = execution_arn.clone();
69 let delivery_clone = delivery.clone();
70 let dynamodb_state_clone = dynamodb_state.clone();
71 let registry_clone = registry.clone();
72 let handle = tokio::spawn(async move {
73 run_states(
74 &def_owned,
75 raw_input,
76 &delivery_clone,
77 &dynamodb_state_clone,
78 ®istry_clone,
79 &state_clone,
80 &execution_arn_clone,
81 )
82 .await
83 });
84
85 match handle.await {
86 Ok(Ok(output)) => {
87 succeed_execution(&state, &execution_arn, &output);
88 }
89 Ok(Err((error, cause))) => {
90 fail_execution(&state, &execution_arn, &error, &cause);
91 }
92 Err(join_err) => {
93 let msg = if join_err.is_panic() {
94 let payload = join_err.into_panic();
95 if let Some(s) = payload.downcast_ref::<String>() {
96 s.clone()
97 } else if let Some(s) = payload.downcast_ref::<&'static str>() {
98 (*s).to_string()
99 } else {
100 "execution task panicked".to_string()
101 }
102 } else {
103 format!("execution task cancelled: {join_err}")
104 };
105 tracing::error!(
106 execution_arn = %execution_arn,
107 panic = %msg,
108 "Step Functions execution panicked"
109 );
110 fail_execution(&state, &execution_arn, "States.Runtime", &msg);
111 }
112 }
113
114 deliver_execution_logs(
116 &state,
117 &execution_arn,
118 delivery.as_ref(),
119 logging_configuration.as_ref(),
120 );
121}
122
123type StatesResult<'a> = std::pin::Pin<
124 Box<dyn std::future::Future<Output = Result<Value, (String, String)>> + Send + 'a>,
125>;
126
127pub(crate) enum Advance {
129 Next(String, Value),
131 End(Value),
133 Fail(String, String),
135}
136
137async fn run_wait_state(
138 name: &str,
139 state_def: &Value,
140 input: Value,
141 shared_state: &SharedStepFunctionsState,
142 execution_arn: &str,
143) -> Advance {
144 let entered_event_id = add_event(
145 shared_state,
146 execution_arn,
147 "WaitStateEntered",
148 0,
149 json!({
150 "name": name,
151 "input": serde_json::to_string(&input).expect("serde_json::Value serialization is infallible"),
152 }),
153 );
154
155 execute_wait_state(state_def, &input).await;
156
157 add_event(
158 shared_state,
159 execution_arn,
160 "WaitStateExited",
161 entered_event_id,
162 json!({
163 "name": name,
164 "output": serde_json::to_string(&input).expect("serde_json::Value serialization is infallible"),
165 }),
166 );
167
168 advance_from_next(state_def, input)
169}
170
171#[allow(clippy::too_many_arguments)]
172async fn run_task_state(
173 name: &str,
174 state_def: &Value,
175 input: Value,
176 delivery: &Option<Arc<DeliveryBus>>,
177 dynamodb_state: &Option<SharedDynamoDbState>,
178 registry: &Option<SharedServiceRegistry>,
179 shared_state: &SharedStepFunctionsState,
180 execution_arn: &str,
181) -> Advance {
182 let entered_event_id = add_event(
183 shared_state,
184 execution_arn,
185 "TaskStateEntered",
186 0,
187 json!({
188 "name": name,
189 "input": serde_json::to_string(&input).expect("serde_json::Value serialization is infallible"),
190 }),
191 );
192
193 let result = execute_task_state(
194 name,
195 state_def,
196 &input,
197 delivery,
198 dynamodb_state,
199 registry,
200 shared_state,
201 execution_arn,
202 entered_event_id,
203 )
204 .await;
205
206 match result {
207 Ok(output) => {
208 add_event(
209 shared_state,
210 execution_arn,
211 "TaskStateExited",
212 entered_event_id,
213 json!({
214 "name": name,
215 "output": serde_json::to_string(&output).expect("serde_json::Value serialization is infallible"),
216 }),
217 );
218 advance_from_next(state_def, output)
219 }
220 Err((error, cause)) => advance_from_error(state_def, &input, error, cause, true),
222 }
223}
224
225#[allow(clippy::too_many_arguments)]
226async fn run_parallel_state(
227 name: &str,
228 state_def: &Value,
229 input: Value,
230 delivery: &Option<Arc<DeliveryBus>>,
231 dynamodb_state: &Option<SharedDynamoDbState>,
232 registry: &Option<SharedServiceRegistry>,
233 shared_state: &SharedStepFunctionsState,
234 execution_arn: &str,
235) -> Advance {
236 let entered_event_id = add_event(
237 shared_state,
238 execution_arn,
239 "ParallelStateEntered",
240 0,
241 json!({
242 "name": name,
243 "input": serde_json::to_string(&input).expect("serde_json::Value serialization is infallible"),
244 }),
245 );
246
247 let result = execute_parallel_state(
248 state_def,
249 &input,
250 delivery,
251 dynamodb_state,
252 registry,
253 shared_state,
254 execution_arn,
255 )
256 .await;
257
258 match result {
259 Ok(output) => {
260 add_event(
261 shared_state,
262 execution_arn,
263 "ParallelStateExited",
264 entered_event_id,
265 json!({
266 "name": name,
267 "output": serde_json::to_string(&output).expect("serde_json::Value serialization is infallible"),
268 }),
269 );
270 advance_from_next(state_def, output)
271 }
272 Err((error, cause)) => advance_from_error(state_def, &input, error, cause, false),
275 }
276}
277
278#[allow(clippy::too_many_arguments)]
279async fn run_map_state(
280 name: &str,
281 state_def: &Value,
282 input: Value,
283 delivery: &Option<Arc<DeliveryBus>>,
284 dynamodb_state: &Option<SharedDynamoDbState>,
285 registry: &Option<SharedServiceRegistry>,
286 shared_state: &SharedStepFunctionsState,
287 execution_arn: &str,
288) -> Advance {
289 let entered_event_id = add_event(
290 shared_state,
291 execution_arn,
292 "MapStateEntered",
293 0,
294 json!({
295 "name": name,
296 "input": serde_json::to_string(&input).expect("serde_json::Value serialization is infallible"),
297 }),
298 );
299
300 let result = execute_map_state(
301 state_def,
302 &input,
303 delivery,
304 dynamodb_state,
305 registry,
306 shared_state,
307 execution_arn,
308 )
309 .await;
310
311 match result {
312 Ok(output) => {
313 add_event(
314 shared_state,
315 execution_arn,
316 "MapStateExited",
317 entered_event_id,
318 json!({
319 "name": name,
320 "output": serde_json::to_string(&output).expect("serde_json::Value serialization is infallible"),
321 }),
322 );
323 advance_from_next(state_def, output)
324 }
325 Err((error, cause)) => advance_from_error(state_def, &input, error, cause, false),
328 }
329}
330
331async fn execute_wait_state(state_def: &Value, input: &Value) {
333 if let Some(seconds) = state_def["Seconds"].as_u64() {
334 tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;
335 return;
336 }
337
338 if let Some(path) = state_def["SecondsPath"].as_str() {
339 let val = crate::io_processing::resolve_path(input, path);
340 if let Some(seconds) = val.as_u64() {
341 tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;
342 }
343 return;
344 }
345
346 if let Some(ts_str) = state_def["Timestamp"].as_str() {
347 if let Ok(target) = chrono::DateTime::parse_from_rfc3339(ts_str) {
348 let now = Utc::now();
349 let target_utc = target.with_timezone(&chrono::Utc);
350 if target_utc > now {
351 let duration = (target_utc - now).to_std().unwrap_or_default();
352 tokio::time::sleep(duration).await;
353 }
354 }
355 return;
356 }
357
358 if let Some(path) = state_def["TimestampPath"].as_str() {
359 let val = crate::io_processing::resolve_path(input, path);
360 if let Some(ts_str) = val.as_str() {
361 if let Ok(target) = chrono::DateTime::parse_from_rfc3339(ts_str) {
362 let now = Utc::now();
363 let target_utc = target.with_timezone(&chrono::Utc);
364 if target_utc > now {
365 let duration = (target_utc - now).to_std().unwrap_or_default();
366 tokio::time::sleep(duration).await;
367 }
368 }
369 }
370 return;
371 }
372
373 warn!(
374 "Wait state has no valid Seconds, SecondsPath, Timestamp, or TimestampPath — skipping wait"
375 );
376}
377
378#[allow(clippy::too_many_arguments)]
381async fn execute_task_state(
382 name: &str,
383 state_def: &Value,
384 input: &Value,
385 delivery: &Option<Arc<DeliveryBus>>,
386 dynamodb_state: &Option<SharedDynamoDbState>,
387 registry: &Option<SharedServiceRegistry>,
388 shared_state: &SharedStepFunctionsState,
389 execution_arn: &str,
390 entered_event_id: i64,
391) -> Result<Value, (String, String)> {
392 let resource = state_def["Resource"].as_str().unwrap_or("").to_string();
393
394 let input_path = state_def["InputPath"].as_str();
395 let result_path = state_def["ResultPath"].as_str();
396 let output_path = state_def["OutputPath"].as_str();
397
398 let effective_input = if input_path == Some("null") {
399 json!({})
400 } else {
401 apply_input_path(input, input_path)
402 };
403
404 let retriers = state_def["Retry"].as_array().cloned().unwrap_or_default();
405 let timeout_seconds = state_def["TimeoutSeconds"].as_u64();
406 let heartbeat_seconds = state_def["HeartbeatSeconds"].as_u64();
407 let mut attempt = 0u32;
408
409 let is_wait_for_task_token = resource.contains(".waitForTaskToken");
410 let task_token = if is_wait_for_task_token {
411 let token = format!(
412 "FCToken-{}-{}",
413 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0),
414 uuid::Uuid::new_v4().simple(),
415 );
416 let account_id = account_id_from_arn(execution_arn);
417 let context = json!({
418 "Task": { "Token": token.clone() },
419 "Execution": { "Id": execution_arn },
420 "State": { "Name": name },
421 });
422 {
423 let mut accounts = shared_state.write();
424 let state = accounts.get_or_create(account_id);
425 state.task_tokens.insert(
426 token.clone(),
427 crate::state::TaskTokenState {
428 activity_arn: String::new(),
429 status: "PENDING".to_string(),
430 output: None,
431 error: None,
432 cause: None,
433 input: None,
434 created_at: chrono::Utc::now(),
435 last_heartbeat_at: None,
436 heartbeat_seconds: heartbeat_seconds.map(|s| s as i64),
437 timeout_seconds: timeout_seconds.map(|s| s as i64),
438 },
439 );
440 }
441 Some((token, context))
442 } else {
443 None
444 };
445
446 let task_input = if let Some(params) = state_def.get("Parameters") {
447 if let Some((_, ctx)) = &task_token {
448 apply_parameters(params, &effective_input, Some(ctx))
449 } else {
450 apply_parameters(params, &effective_input, None)
451 }
452 } else {
453 effective_input
454 };
455
456 loop {
457 add_event(
458 shared_state,
459 execution_arn,
460 "TaskScheduled",
461 entered_event_id,
462 json!({
463 "resource": resource,
464 "region": "us-east-1",
465 "parameters": serde_json::to_string(&task_input).expect("serde_json::Value serialization is infallible"),
466 }),
467 );
468
469 add_event(
470 shared_state,
471 execution_arn,
472 "TaskStarted",
473 entered_event_id,
474 json!({ "resource": resource }),
475 );
476
477 let invoke_result = invoke_resource(
478 &resource,
479 &task_input,
480 delivery,
481 dynamodb_state,
482 registry,
483 execution_arn,
484 timeout_seconds,
485 heartbeat_seconds,
486 shared_state,
487 )
488 .await;
489
490 match invoke_result {
491 Ok(result) => {
492 if let Some((token, _)) = &task_token {
493 let account_id = account_id_from_arn(execution_arn);
494 match poll_task_token(
495 shared_state,
496 account_id,
497 token,
498 timeout_seconds,
499 heartbeat_seconds,
500 )
501 .await
502 {
503 Ok(output) => {
504 add_event(
505 shared_state,
506 execution_arn,
507 "TaskSucceeded",
508 entered_event_id,
509 json!({
510 "resource": resource,
511 "output": serde_json::to_string(&output).expect("serde_json::Value serialization is infallible"),
512 }),
513 );
514
515 let selected = if let Some(selector) = state_def.get("ResultSelector") {
516 apply_parameters(selector, &output, None)
517 } else {
518 output
519 };
520
521 let after_result = if result_path == Some("null") {
522 input.clone()
523 } else {
524 apply_result_path(input, &selected, result_path)
525 };
526
527 let output = if output_path == Some("null") {
528 json!({})
529 } else {
530 apply_output_path(&after_result, output_path)
531 };
532
533 return Ok(output);
534 }
535 Err((error, cause)) => {
536 add_event(
537 shared_state,
538 execution_arn,
539 "TaskFailed",
540 entered_event_id,
541 json!({ "error": error, "cause": cause }),
542 );
543
544 if let Some(delay_ms) = should_retry(&retriers, &error, attempt, true) {
545 attempt += 1;
546 tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms))
549 .await;
550 continue;
551 }
552
553 return Err((error, cause));
554 }
555 }
556 }
557
558 add_event(
559 shared_state,
560 execution_arn,
561 "TaskSucceeded",
562 entered_event_id,
563 json!({
564 "resource": resource,
565 "output": serde_json::to_string(&result).expect("serde_json::Value serialization is infallible"),
566 }),
567 );
568
569 let selected = if let Some(selector) = state_def.get("ResultSelector") {
570 apply_parameters(selector, &result, None)
571 } else {
572 result
573 };
574
575 let after_result = if result_path == Some("null") {
576 input.clone()
577 } else {
578 apply_result_path(input, &selected, result_path)
579 };
580
581 let output = if output_path == Some("null") {
582 json!({})
583 } else {
584 apply_output_path(&after_result, output_path)
585 };
586
587 return Ok(output);
588 }
589 Err((error, cause)) => {
590 add_event(
591 shared_state,
592 execution_arn,
593 "TaskFailed",
594 entered_event_id,
595 json!({ "error": error, "cause": cause }),
596 );
597
598 if let Some(delay_ms) = should_retry(&retriers, &error, attempt, true) {
599 attempt += 1;
600 tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
603 continue;
604 }
605
606 return Err((error, cause));
607 }
608 }
609 }
610}
611
612async fn execute_parallel_state(
614 state_def: &Value,
615 input: &Value,
616 delivery: &Option<Arc<DeliveryBus>>,
617 dynamodb_state: &Option<SharedDynamoDbState>,
618 registry: &Option<SharedServiceRegistry>,
619 shared_state: &SharedStepFunctionsState,
620 execution_arn: &str,
621) -> Result<Value, (String, String)> {
622 let input_path = state_def["InputPath"].as_str();
623 let result_path = state_def["ResultPath"].as_str();
624 let output_path = state_def["OutputPath"].as_str();
625
626 let effective_input = if input_path == Some("null") {
627 json!({})
628 } else {
629 apply_input_path(input, input_path)
630 };
631
632 let branches = state_def["Branches"]
633 .as_array()
634 .cloned()
635 .unwrap_or_default();
636
637 if branches.is_empty() {
638 return Err((
639 "States.Runtime".to_string(),
640 "Parallel state has no Branches".to_string(),
641 ));
642 }
643
644 let mut handles = Vec::new();
646 for branch_def in &branches {
647 let branch = branch_def.clone();
648 let branch_input = effective_input.clone();
649 let delivery = delivery.clone();
650 let ddb = dynamodb_state.clone();
651 let reg = registry.clone();
652 let state = shared_state.clone();
653 let arn = execution_arn.to_string();
654
655 handles.push(tokio::spawn(async move {
656 run_states(&branch, branch_input, &delivery, &ddb, ®, &state, &arn).await
657 }));
658 }
659
660 let mut results = Vec::with_capacity(handles.len());
662 for handle in handles {
663 let result = handle.await.map_err(|e| {
664 (
665 "States.Runtime".to_string(),
666 format!("Branch execution panicked: {e}"),
667 )
668 })??;
669 results.push(result);
670 }
671
672 let branch_output = Value::Array(results);
673
674 let selected = if let Some(selector) = state_def.get("ResultSelector") {
676 apply_parameters(selector, &branch_output, None)
677 } else {
678 branch_output
679 };
680
681 let after_result = if result_path == Some("null") {
683 input.clone()
684 } else {
685 apply_result_path(input, &selected, result_path)
686 };
687
688 let output = if output_path == Some("null") {
690 json!({})
691 } else {
692 apply_output_path(&after_result, output_path)
693 };
694
695 Ok(output)
696}
697
698#[allow(clippy::too_many_arguments)]
702async fn execute_map_state(
703 state_def: &Value,
704 input: &Value,
705 delivery: &Option<Arc<DeliveryBus>>,
706 dynamodb_state: &Option<SharedDynamoDbState>,
707 registry: &Option<SharedServiceRegistry>,
708 shared_state: &SharedStepFunctionsState,
709 execution_arn: &str,
710) -> Result<Value, (String, String)> {
711 let input_path = state_def["InputPath"].as_str();
712 let result_path = state_def["ResultPath"].as_str();
713 let output_path = state_def["OutputPath"].as_str();
714
715 let effective_input = if input_path == Some("null") {
716 json!({})
717 } else {
718 apply_input_path(input, input_path)
719 };
720
721 let max_concurrency = if let Some(path) = state_def["MaxConcurrencyPath"].as_str() {
723 crate::io_processing::resolve_path(&effective_input, path)
724 .as_u64()
725 .unwrap_or(0)
726 } else {
727 state_def["MaxConcurrency"].as_u64().unwrap_or(0)
728 };
729 let effective_concurrency = if max_concurrency == 0 {
730 40
731 } else {
732 max_concurrency as usize
733 };
734
735 let items = if let Some(item_reader) = state_def.get("ItemReader") {
737 read_items_from_s3(item_reader, registry, execution_arn).await?
738 } else {
739 let items_path = state_def["ItemsPath"].as_str().unwrap_or("$");
740 let items_value = crate::io_processing::resolve_path(&effective_input, items_path);
741 items_value.as_array().cloned().unwrap_or_default()
742 };
743
744 let batch_config = state_def.get("ItemBatcher").cloned();
746 let batched_items = if let Some(ref batcher) = batch_config {
747 apply_item_batcher(&items, batcher, &effective_input)
748 } else {
749 items
750 };
751
752 let iterator_def = state_def
754 .get("ItemProcessor")
755 .or_else(|| state_def.get("Iterator"))
756 .cloned()
757 .ok_or_else(|| {
758 (
759 "States.Runtime".to_string(),
760 "Map state has no ItemProcessor or Iterator".to_string(),
761 )
762 })?;
763
764 let tolerated_failure_percentage = state_def["ToleratedFailurePercentage"]
765 .as_f64()
766 .unwrap_or(0.0);
767 let tolerated_failure_count = state_def["ToleratedFailureCount"].as_i64().unwrap_or(0);
768 let total_items = batched_items.len() as f64;
769 let mut failure_count = 0usize;
770
771 let is_distributed = iterator_def
775 .get("ProcessorConfig")
776 .and_then(|c| c.get("Mode"))
777 .and_then(Value::as_str)
778 == Some("DISTRIBUTED");
779 let map_run_arn = if is_distributed {
780 map_run_arn_for(execution_arn).inspect(|arn| {
781 start_map_run(
782 shared_state,
783 arn,
784 execution_arn,
785 max_concurrency as usize,
789 tolerated_failure_percentage,
790 tolerated_failure_count,
791 batched_items.len(),
792 );
793 })
794 } else {
795 None
796 };
797
798 let semaphore = Arc::new(tokio::sync::Semaphore::new(effective_concurrency));
799
800 let mut handles = Vec::new();
802 for (index, batch_item) in batched_items.into_iter().enumerate() {
803 let iter_def = iterator_def.clone();
804 let delivery = delivery.clone();
805 let ddb = dynamodb_state.clone();
806 let reg = registry.clone();
807 let state = shared_state.clone();
808 let arn = execution_arn.to_string();
809 let sem = semaphore.clone();
810
811 let item_input = if let Some(selector) = state_def.get("ItemSelector") {
813 let mut ctx = serde_json::Map::new();
814 ctx.insert("value".to_string(), batch_item.clone());
815 ctx.insert("index".to_string(), json!(index));
816 apply_parameters(selector, &Value::Object(ctx), None)
817 } else {
818 batch_item
819 };
820
821 add_event(
822 shared_state,
823 execution_arn,
824 "MapIterationStarted",
825 0,
826 json!({ "index": index }),
827 );
828
829 handles.push(tokio::spawn(async move {
830 let _permit = sem
831 .acquire()
832 .await
833 .map_err(|_| ("States.Runtime".to_string(), "Semaphore closed".to_string()))?;
834 let result =
835 run_states(&iter_def, item_input, &delivery, &ddb, ®, &state, &arn).await;
836 Ok::<(usize, Result<Value, (String, String)>), (String, String)>((index, result))
837 }));
838 }
839
840 let mut results: Vec<(usize, Value)> = Vec::with_capacity(handles.len());
842 for handle in handles {
843 let (index, result) = handle.await.map_err(|e| {
844 (
845 "States.Runtime".to_string(),
846 format!("Map iteration panicked: {e}"),
847 )
848 })??;
849
850 match result {
851 Ok(output) => {
852 add_event(
853 shared_state,
854 execution_arn,
855 "MapIterationSucceeded",
856 0,
857 json!({ "index": index }),
858 );
859 results.push((index, output));
860 }
861 Err((error, cause)) => {
862 add_event(
863 shared_state,
864 execution_arn,
865 "MapIterationFailed",
866 0,
867 json!({ "index": index, "error": error }),
868 );
869 failure_count += 1;
870 let failure_percentage = (failure_count as f64 / total_items) * 100.0;
871 if failure_percentage > tolerated_failure_percentage {
872 if let Some(arn) = &map_run_arn {
873 finish_map_run(
874 shared_state,
875 arn,
876 execution_arn,
877 results.len() as i64,
878 failure_count as i64,
879 "FAILED",
880 );
881 }
882 return Err((error, cause));
883 }
884 }
885 }
886 }
887
888 if let Some(arn) = &map_run_arn {
891 finish_map_run(
892 shared_state,
893 arn,
894 execution_arn,
895 results.len() as i64,
896 failure_count as i64,
897 "SUCCEEDED",
898 );
899 }
900
901 results.sort_by_key(|(i, _)| *i);
903 let map_output = Value::Array(results.into_iter().map(|(_, v)| v).collect());
904
905 if let Some(result_writer) = state_def.get("ResultWriter") {
907 write_map_results_to_s3(result_writer, registry, execution_arn, &map_output).await?;
908 }
909
910 let selected = if let Some(selector) = state_def.get("ResultSelector") {
912 apply_parameters(selector, &map_output, None)
913 } else {
914 map_output
915 };
916
917 let after_result = if result_path == Some("null") {
919 input.clone()
920 } else {
921 apply_result_path(input, &selected, result_path)
922 };
923
924 let output = if output_path == Some("null") {
926 json!({})
927 } else {
928 apply_output_path(&after_result, output_path)
929 };
930
931 Ok(output)
932}
933
934fn map_run_arn_for(execution_arn: &str) -> Option<String> {
937 let (prefix, tail) = execution_arn.split_once(":execution:")?;
938 let sm_exec = tail.replacen(':', "/", 1);
941 let label = uuid::Uuid::new_v4().simple().to_string();
942 Some(format!("{prefix}:mapRun:{sm_exec}:{label}"))
943}
944
945fn start_map_run(
947 shared_state: &SharedStepFunctionsState,
948 map_run_arn: &str,
949 execution_arn: &str,
950 max_concurrency: usize,
951 tolerated_failure_percentage: f64,
952 tolerated_failure_count: i64,
953 total: usize,
954) {
955 let account = account_id_from_arn(execution_arn).to_string();
956 let mut accounts = shared_state.write();
957 let s = accounts.get_or_create(&account);
958 s.map_runs.insert(
959 map_run_arn.to_string(),
960 MapRun {
961 map_run_arn: map_run_arn.to_string(),
962 execution_arn: execution_arn.to_string(),
963 max_concurrency: max_concurrency as i32,
964 tolerated_failure_percentage,
965 tolerated_failure_count,
966 status: "RUNNING".to_string(),
967 start_date: Utc::now(),
968 stop_date: None,
969 total_count: total as i64,
970 succeeded_count: 0,
971 failed_count: 0,
972 },
973 );
974}
975
976fn finish_map_run(
979 shared_state: &SharedStepFunctionsState,
980 map_run_arn: &str,
981 execution_arn: &str,
982 succeeded: i64,
983 failed: i64,
984 status: &str,
985) {
986 let account = account_id_from_arn(execution_arn).to_string();
987 let mut accounts = shared_state.write();
988 let s = accounts.get_or_create(&account);
989 if let Some(mr) = s.map_runs.get_mut(map_run_arn) {
990 mr.status = status.to_string();
991 mr.succeeded_count = succeeded;
992 mr.failed_count = failed;
993 mr.stop_date = Some(Utc::now());
994 }
995}
996
997async fn read_items_from_s3(
999 item_reader: &Value,
1000 registry: &Option<SharedServiceRegistry>,
1001 execution_arn: &str,
1002) -> Result<Vec<Value>, (String, String)> {
1003 let resource = item_reader["Resource"]
1004 .as_str()
1005 .unwrap_or("arn:aws:states:::s3:getObject");
1006 if !resource.contains("s3:getObject") {
1007 return Err((
1008 "States.Runtime".to_string(),
1009 format!("ItemReader unsupported resource: {resource}"),
1010 ));
1011 }
1012
1013 let params = item_reader
1014 .get("Parameters")
1015 .cloned()
1016 .unwrap_or_else(|| json!({}));
1017 let bucket = params["Bucket"].as_str().ok_or_else(|| {
1018 (
1019 "States.Runtime".to_string(),
1020 "ItemReader missing Bucket".to_string(),
1021 )
1022 })?;
1023 let key = params["Key"].as_str().ok_or_else(|| {
1024 (
1025 "States.Runtime".to_string(),
1026 "ItemReader missing Key".to_string(),
1027 )
1028 })?;
1029
1030 let registry_arc = resolve_registry(registry)?;
1031 let account_id = account_from_execution_arn(execution_arn);
1032
1033 let body = call_sdk_action_raw_bytes(
1034 ®istry_arc,
1035 "s3",
1036 "GetObject",
1037 &json!({ "Bucket": bucket, "Key": key }),
1038 &account_id,
1039 )
1040 .await?;
1041
1042 let parsed: Value = serde_json::from_slice(&body).map_err(|e| {
1044 (
1045 "States.Runtime".to_string(),
1046 format!("ItemReader failed to parse S3 object as JSON: {e}"),
1047 )
1048 })?;
1049
1050 parsed.as_array().cloned().ok_or_else(|| {
1051 (
1052 "States.Runtime".to_string(),
1053 "ItemReader S3 object is not a JSON array".to_string(),
1054 )
1055 })
1056}
1057
1058async fn write_map_results_to_s3(
1060 result_writer: &Value,
1061 registry: &Option<SharedServiceRegistry>,
1062 execution_arn: &str,
1063 results: &Value,
1064) -> Result<(), (String, String)> {
1065 let resource = result_writer["Resource"]
1066 .as_str()
1067 .unwrap_or("arn:aws:states:::s3:putObject");
1068 if !resource.contains("s3:putObject") {
1069 return Err((
1070 "States.Runtime".to_string(),
1071 format!("ResultWriter unsupported resource: {resource}"),
1072 ));
1073 }
1074
1075 let params = result_writer
1076 .get("Parameters")
1077 .cloned()
1078 .unwrap_or_else(|| json!({}));
1079 let bucket = params["Bucket"].as_str().ok_or_else(|| {
1080 (
1081 "States.Runtime".to_string(),
1082 "ResultWriter missing Bucket".to_string(),
1083 )
1084 })?;
1085 let prefix = params["Prefix"].as_str().unwrap_or("map-results/");
1086
1087 let registry_arc = resolve_registry(registry)?;
1088 let account_id = account_from_execution_arn(execution_arn);
1089
1090 use bytes::Bytes;
1091 let body = Bytes::from(
1092 serde_json::to_vec(results).expect("serde_json::Value serialization is infallible"),
1093 );
1094
1095 use fakecloud_core::service::AwsRequest;
1097 use http::{HeaderMap, Method};
1098 let service = registry_arc.get("s3").ok_or_else(|| {
1099 (
1100 "States.TaskFailed".to_string(),
1101 "S3 service not available for ResultWriter".to_string(),
1102 )
1103 })?;
1104
1105 let req = AwsRequest {
1106 service: "s3".to_string(),
1107 action: "PutObject".to_string(),
1108 region: "us-east-1".to_string(),
1109 account_id: account_id.to_string(),
1110 request_id: uuid::Uuid::new_v4().to_string(),
1111 headers: HeaderMap::new(),
1112 query_params: std::collections::HashMap::new(),
1113 body,
1114 body_stream: parking_lot::Mutex::new(None),
1115 path_segments: vec![bucket.to_string(), format!("{prefix}result.json")],
1116 raw_path: format!("/{bucket}/{prefix}result.json"),
1117 raw_query: String::new(),
1118 method: Method::PUT,
1119 is_query_protocol: false,
1120 access_key_id: None,
1121 principal: None,
1122 };
1123
1124 service.handle(req).await.map_err(|err| {
1125 let code = err.code().to_string();
1126 let msg = err.message();
1127 (
1128 format!("S3.{code}"),
1129 format!("ResultWriter PutObject failed: {msg}"),
1130 )
1131 })?;
1132
1133 Ok(())
1134}
1135
1136fn apply_item_batcher(items: &[Value], batcher: &Value, _effective_input: &Value) -> Vec<Value> {
1138 let max_per_batch = batcher["MaxItemsPerBatch"].as_u64().unwrap_or(u64::MAX) as usize;
1139 let max_bytes = batcher["MaxInputBytesPerBatch"].as_u64().unwrap_or(0) as usize;
1140 let batch_input = batcher.get("BatchInput").cloned();
1141
1142 let mut batches: Vec<Vec<Value>> = Vec::new();
1143 let mut current_batch: Vec<Value> = Vec::new();
1144 let mut current_bytes = 0usize;
1145
1146 for item in items.iter().cloned() {
1147 let item_bytes = serde_json::to_vec(&item).unwrap_or_default().len();
1148 if !current_batch.is_empty()
1149 && (current_batch.len() >= max_per_batch
1150 || (max_bytes > 0 && current_bytes + item_bytes > max_bytes))
1151 {
1152 batches.push(current_batch);
1153 current_batch = Vec::new();
1154 current_bytes = 0;
1155 }
1156 current_bytes += item_bytes;
1157 current_batch.push(item);
1158 }
1159 if !current_batch.is_empty() {
1160 batches.push(current_batch);
1161 }
1162
1163 batches
1164 .into_iter()
1165 .enumerate()
1166 .map(|(index, batch)| {
1167 let mut map = serde_json::Map::new();
1168 map.insert("index".to_string(), json!(index));
1169 map.insert("items".to_string(), Value::Array(batch));
1170 if let Some(Value::Object(ref obj)) = batch_input {
1171 for (k, v) in obj {
1172 map.insert(k.clone(), v.clone());
1173 }
1174 }
1175 Value::Object(map)
1176 })
1177 .collect()
1178}
1179
1180#[allow(clippy::too_many_arguments)]
1182async fn invoke_resource(
1183 resource: &str,
1184 input: &Value,
1185 delivery: &Option<Arc<DeliveryBus>>,
1186 dynamodb_state: &Option<SharedDynamoDbState>,
1187 registry: &Option<SharedServiceRegistry>,
1188 execution_arn: &str,
1189 timeout_seconds: Option<u64>,
1190 heartbeat_seconds: Option<u64>,
1191 shared_state: &SharedStepFunctionsState,
1192) -> Result<Value, (String, String)> {
1193 if resource.contains(":states:") && resource.contains(":activity:") {
1195 return invoke_activity(
1196 resource,
1197 input,
1198 shared_state,
1199 timeout_seconds,
1200 heartbeat_seconds,
1201 )
1202 .await;
1203 }
1204
1205 if resource.contains(":lambda:") && resource.contains(":function:") {
1207 return invoke_lambda_direct(resource, input, delivery, timeout_seconds).await;
1208 }
1209
1210 if resource.starts_with("arn:aws:states:::lambda:invoke") {
1212 let function_name = input["FunctionName"].as_str().unwrap_or("");
1213 let payload = if let Some(p) = input.get("Payload") {
1214 p.clone()
1215 } else {
1216 input.clone()
1217 };
1218 return invoke_lambda_direct(function_name, &payload, delivery, timeout_seconds)
1228 .await
1229 .map(|payload| {
1230 json!({
1231 "ExecutedVersion": "$LATEST",
1232 "Payload": payload,
1233 "StatusCode": 200,
1234 })
1235 });
1236 }
1237
1238 if resource.starts_with("arn:aws:states:::sqs:sendMessage") {
1239 return invoke_sqs_send_message(input, delivery);
1240 }
1241
1242 if resource.starts_with("arn:aws:states:::sns:publish") {
1243 return invoke_sns_publish(input, delivery);
1244 }
1245
1246 if resource.starts_with("arn:aws:states:::events:putEvents") {
1247 return invoke_eventbridge_put_events(input, delivery);
1248 }
1249
1250 if resource.starts_with("arn:aws:states:::dynamodb:getItem") {
1251 return invoke_dynamodb_get_item(input, dynamodb_state);
1252 }
1253
1254 if resource.starts_with("arn:aws:states:::dynamodb:putItem") {
1255 return invoke_dynamodb_put_item(input, dynamodb_state);
1256 }
1257
1258 if resource.starts_with("arn:aws:states:::dynamodb:deleteItem") {
1259 return invoke_dynamodb_delete_item(input, dynamodb_state);
1260 }
1261
1262 if resource.starts_with("arn:aws:states:::dynamodb:updateItem") {
1263 return invoke_dynamodb_update_item(input, dynamodb_state);
1264 }
1265
1266 if let Some(tail) = resource.strip_prefix("arn:aws:states:::") {
1274 if tail.starts_with("states:startExecution") {
1275 let account_id = account_from_execution_arn(execution_arn);
1276 let result =
1277 invoke_aws_sdk_integration(tail, input, registry, &account_id, timeout_seconds)
1278 .await;
1279 if let Ok(ref value) = result {
1285 if let Some(inner_arn) = value
1286 .get("executionArn")
1287 .or_else(|| value.get("ExecutionArn"))
1288 .and_then(Value::as_str)
1289 {
1290 let mut accounts = shared_state.write();
1291 if let Some(state) = accounts.get_mut(&account_id) {
1292 if let Some(exec) = state.executions.get_mut(inner_arn) {
1293 exec.parent_execution_arn = Some(execution_arn.to_string());
1294 }
1295 }
1296 }
1297 }
1298 return result;
1299 }
1300 }
1301
1302 if let Some(rest) = resource.strip_prefix("arn:aws:states:::aws-sdk:") {
1308 let account_id = account_from_execution_arn(execution_arn);
1309 return invoke_aws_sdk_integration(rest, input, registry, &account_id, timeout_seconds)
1310 .await;
1311 }
1312
1313 if let Some(tail) = resource.strip_prefix("arn:aws:states:::") {
1317 if tail.contains(".sync") {
1318 let account_id = account_from_execution_arn(execution_arn);
1319 return invoke_aws_sdk_integration(tail, input, registry, &account_id, timeout_seconds)
1320 .await;
1321 }
1322 }
1323
1324 Err((
1325 "States.TaskFailed".to_string(),
1326 format!("Unsupported resource: {resource}"),
1327 ))
1328}
1329
1330fn camel_to_pascal(action: &str) -> String {
1335 let mut chars = action.chars();
1336 match chars.next() {
1337 None => String::new(),
1338 Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
1339 }
1340}
1341
1342fn map_sdk_service_id(service_id: &str) -> &str {
1347 match service_id {
1348 "sfn" => "states",
1349 "cloudwatchlogs" => "logs",
1350 other => other,
1352 }
1353}
1354
1355fn account_from_execution_arn(execution_arn: &str) -> String {
1359 execution_arn
1360 .split(':')
1361 .nth(4)
1362 .filter(|s| !s.is_empty())
1363 .unwrap_or("123456789012")
1364 .to_string()
1365}
1366
1367async fn invoke_aws_sdk_integration(
1372 tail: &str,
1373 input: &Value,
1374 registry: &Option<SharedServiceRegistry>,
1375 account_id: &str,
1376 timeout_seconds: Option<u64>,
1377) -> Result<Value, (String, String)> {
1378 let registry_arc = resolve_registry(registry)?;
1379
1380 let mut parts = tail.splitn(2, ':');
1386 let service_id = parts.next().filter(|s| !s.is_empty()).ok_or_else(|| {
1387 (
1388 "States.TaskFailed".to_string(),
1389 format!("Invalid aws-sdk Resource ARN: missing service in '{tail}'"),
1390 )
1391 })?;
1392 let action_with_mod = parts.next().filter(|s| !s.is_empty()).ok_or_else(|| {
1393 (
1394 "States.TaskFailed".to_string(),
1395 format!("Invalid aws-sdk Resource ARN: missing action in '{tail}'"),
1396 )
1397 })?;
1398 let action_camel = action_with_mod
1399 .split('.')
1400 .next()
1401 .filter(|s| !s.is_empty())
1402 .ok_or_else(|| {
1403 (
1404 "States.TaskFailed".to_string(),
1405 format!("Invalid aws-sdk Resource ARN: empty action in '{tail}'"),
1406 )
1407 })?;
1408 let modifiers: Vec<&str> = action_with_mod.split('.').skip(1).collect();
1409 let is_sync = modifiers.iter().any(|m| *m == "sync" || *m == "sync:2");
1410
1411 let action_pascal = camel_to_pascal(action_camel);
1412 let service_name = map_sdk_service_id(service_id).to_string();
1413
1414 let translated_input = match service_name.as_str() {
1419 "ecs" => translate_ecs_keys_to_camel(input),
1420 _ => input.clone(),
1421 };
1422
1423 let initial = call_sdk_action(
1424 ®istry_arc,
1425 &service_name,
1426 &action_pascal,
1427 &translated_input,
1428 account_id,
1429 )
1430 .await?;
1431
1432 if !is_sync {
1433 return Ok(initial);
1434 }
1435
1436 sync_wait(
1441 ®istry_arc,
1442 &service_name,
1443 &action_pascal,
1444 &initial,
1445 &translated_input,
1446 account_id,
1447 timeout_seconds,
1448 )
1449 .await
1450}
1451
1452fn translate_ecs_keys_to_camel(input: &Value) -> Value {
1459 let Some(obj) = input.as_object() else {
1460 return input.clone();
1461 };
1462 let mut out = serde_json::Map::with_capacity(obj.len());
1463 for (k, v) in obj.iter() {
1464 let camel = match k.as_str() {
1465 "Cluster" => "cluster",
1466 "TaskDefinition" => "taskDefinition",
1467 "LaunchType" => "launchType",
1468 "Group" => "group",
1469 "Overrides" => "overrides",
1470 "PlatformVersion" => "platformVersion",
1471 "NetworkConfiguration" => "networkConfiguration",
1472 "Tags" => "tags",
1473 "EnableExecuteCommand" => "enableExecuteCommand",
1474 "PropagateTags" => "propagateTags",
1475 "ReferenceId" => "referenceId",
1476 "StartedBy" => "startedBy",
1477 "Count" => "count",
1478 "CapacityProviderStrategy" => "capacityProviderStrategy",
1479 "PlacementConstraints" => "placementConstraints",
1480 "PlacementStrategy" => "placementStrategy",
1481 other => other,
1482 };
1483 out.insert(camel.to_string(), v.clone());
1484 }
1485 Value::Object(out)
1486}
1487
1488fn resolve_registry(
1489 registry: &Option<SharedServiceRegistry>,
1490) -> Result<Arc<fakecloud_core::registry::ServiceRegistry>, (String, String)> {
1491 let registry_handle = registry.as_ref().ok_or_else(|| {
1492 (
1493 "States.TaskFailed".to_string(),
1494 "No service registry configured for aws-sdk integration".to_string(),
1495 )
1496 })?;
1497 registry_handle.get().cloned().ok_or_else(|| {
1498 (
1499 "States.TaskFailed".to_string(),
1500 "Service registry not yet initialised; aws-sdk integration unavailable".to_string(),
1501 )
1502 })
1503}
1504
1505async fn call_sdk_action(
1507 registry: &Arc<fakecloud_core::registry::ServiceRegistry>,
1508 service_name: &str,
1509 action_pascal: &str,
1510 input: &Value,
1511 account_id: &str,
1512) -> Result<Value, (String, String)> {
1513 use bytes::Bytes;
1514 use fakecloud_core::service::AwsRequest;
1515 use http::{HeaderMap, Method};
1516
1517 let service = registry.get(service_name).ok_or_else(|| {
1518 (
1519 "States.TaskFailed".to_string(),
1520 format!("Unknown aws-sdk service '{service_name}'"),
1521 )
1522 })?;
1523
1524 let body_bytes = Bytes::from(
1525 serde_json::to_vec(input).expect("serde_json::Value serialization is infallible"),
1526 );
1527
1528 let req = AwsRequest {
1529 service: service_name.to_string(),
1530 action: action_pascal.to_string(),
1531 region: "us-east-1".to_string(),
1532 account_id: account_id.to_string(),
1533 request_id: uuid::Uuid::new_v4().to_string(),
1534 headers: HeaderMap::new(),
1535 query_params: std::collections::HashMap::new(),
1536 body: body_bytes,
1537 body_stream: parking_lot::Mutex::new(None),
1538 path_segments: vec![],
1539 raw_path: "/".to_string(),
1540 raw_query: String::new(),
1541 method: Method::POST,
1542 is_query_protocol: false,
1543 access_key_id: None,
1544 principal: None,
1545 };
1546
1547 let response = service.handle(req).await.map_err(|err| {
1548 let code = err.code().to_string();
1549 let msg = err.message();
1550 let prefix_service = match service_name {
1551 "dynamodb" => "DynamoDb".to_string(),
1552 "states" => "Sfn".to_string(),
1553 other => camel_to_pascal(other),
1554 };
1555 (
1556 format!("{prefix_service}.{code}"),
1557 format!("{action_pascal} failed: {msg}"),
1558 )
1559 })?;
1560
1561 let response_bytes = match response.body {
1562 fakecloud_core::service::ResponseBody::Bytes(b) => b,
1563 fakecloud_core::service::ResponseBody::File { .. } => {
1564 return Err((
1565 "States.TaskFailed".to_string(),
1566 "aws-sdk integration: file-backed response not supported".to_string(),
1567 ));
1568 }
1569 };
1570
1571 if response_bytes.is_empty() {
1572 return Ok(json!({}));
1573 }
1574 serde_json::from_slice(&response_bytes).map_err(|e| {
1575 (
1576 "States.TaskFailed".to_string(),
1577 format!("aws-sdk integration: failed to parse response JSON: {e}"),
1578 )
1579 })
1580}
1581
1582async fn call_sdk_action_raw_bytes(
1585 registry: &Arc<fakecloud_core::registry::ServiceRegistry>,
1586 service_name: &str,
1587 action_pascal: &str,
1588 input: &Value,
1589 account_id: &str,
1590) -> Result<bytes::Bytes, (String, String)> {
1591 use bytes::Bytes;
1592 use fakecloud_core::service::AwsRequest;
1593 use http::{HeaderMap, Method};
1594
1595 let service = registry.get(service_name).ok_or_else(|| {
1596 (
1597 "States.TaskFailed".to_string(),
1598 format!("Unknown aws-sdk service '{service_name}'"),
1599 )
1600 })?;
1601
1602 let body_bytes = Bytes::from(
1603 serde_json::to_vec(input).expect("serde_json::Value serialization is infallible"),
1604 );
1605
1606 let req = AwsRequest {
1607 service: service_name.to_string(),
1608 action: action_pascal.to_string(),
1609 region: "us-east-1".to_string(),
1610 account_id: account_id.to_string(),
1611 request_id: uuid::Uuid::new_v4().to_string(),
1612 headers: HeaderMap::new(),
1613 query_params: std::collections::HashMap::new(),
1614 body: body_bytes,
1615 body_stream: parking_lot::Mutex::new(None),
1616 path_segments: vec![],
1617 raw_path: "/".to_string(),
1618 raw_query: String::new(),
1619 method: Method::POST,
1620 is_query_protocol: false,
1621 access_key_id: None,
1622 principal: None,
1623 };
1624
1625 let response = service.handle(req).await.map_err(|err| {
1626 let code = err.code().to_string();
1627 let msg = err.message();
1628 let prefix_service = match service_name {
1629 "dynamodb" => "DynamoDb".to_string(),
1630 "states" => "Sfn".to_string(),
1631 other => camel_to_pascal(other),
1632 };
1633 (
1634 format!("{prefix_service}.{code}"),
1635 format!("{action_pascal} failed: {msg}"),
1636 )
1637 })?;
1638
1639 match response.body {
1640 fakecloud_core::service::ResponseBody::Bytes(b) => Ok(b),
1641 fakecloud_core::service::ResponseBody::File { .. } => Err((
1642 "States.TaskFailed".to_string(),
1643 "aws-sdk integration: file-backed response not supported".to_string(),
1644 )),
1645 }
1646}
1647
1648const SYNC_DEFAULT_TIMEOUT_SECS: u64 = 300;
1654const SYNC_POLL_INTERVAL_MS: u64 = 200;
1655
1656async fn sync_wait(
1660 registry: &Arc<fakecloud_core::registry::ServiceRegistry>,
1661 service_name: &str,
1662 action_pascal: &str,
1663 initial: &Value,
1664 input: &Value,
1665 account_id: &str,
1666 timeout_seconds: Option<u64>,
1667) -> Result<Value, (String, String)> {
1668 match (service_name, action_pascal) {
1669 ("ecs", "RunTask") => {
1670 sync_wait_ecs_run_task(registry, initial, input, account_id, timeout_seconds).await
1671 }
1672 ("athena", "StartQueryExecution") => {
1673 sync_wait_athena_query(registry, initial, account_id, timeout_seconds).await
1674 }
1675 ("states", "StartExecution") => {
1676 sync_wait_states_start_execution(registry, initial, account_id, timeout_seconds).await
1677 }
1678 ("glue", "StartJobRun") => {
1679 let job_run_id = initial
1685 .get("JobRunId")
1686 .and_then(Value::as_str)
1687 .unwrap_or_default()
1688 .to_string();
1689 let job_name = input
1690 .get("JobName")
1691 .and_then(Value::as_str)
1692 .unwrap_or_default()
1693 .to_string();
1694 let deadline = sync_deadline(timeout_seconds);
1695 loop {
1696 let described = call_sdk_action(
1697 registry,
1698 "glue",
1699 "GetJobRun",
1700 &json!({ "JobName": job_name, "RunId": job_run_id }),
1701 account_id,
1702 )
1703 .await?;
1704 let state = described
1705 .get("JobRun")
1706 .and_then(|r| r.get("JobRunState"))
1707 .and_then(Value::as_str)
1708 .unwrap_or("");
1709 match state {
1710 "SUCCEEDED" => return Ok(described),
1711 "FAILED" | "STOPPED" | "TIMEOUT" | "ERROR" => {
1712 return Err((
1713 "States.TaskFailed".to_string(),
1714 format!("Glue job run {job_run_id} ended in state {state}"),
1715 ));
1716 }
1717 _ => {}
1718 }
1719 if std::time::Instant::now() >= deadline {
1720 return Err((
1721 "States.Timeout".to_string(),
1722 format!(
1723 "glue:startJobRun.sync timed out after {}s for run {job_run_id}",
1724 sync_timeout_secs(timeout_seconds)
1725 ),
1726 ));
1727 }
1728 tokio::time::sleep(std::time::Duration::from_millis(SYNC_POLL_INTERVAL_MS)).await;
1729 }
1730 }
1731 _ => Err((
1732 "States.TaskFailed".to_string(),
1733 format!(
1734 "`.sync` is not supported for {service_name}:{action_pascal} yet — \
1735 supported: ecs:RunTask, athena:StartQueryExecution, glue:StartJobRun, states:StartExecution"
1736 ),
1737 )),
1738 }
1739}
1740
1741async fn sync_wait_ecs_run_task(
1742 registry: &Arc<fakecloud_core::registry::ServiceRegistry>,
1743 initial: &Value,
1744 input: &Value,
1745 account_id: &str,
1746 timeout_seconds: Option<u64>,
1747) -> Result<Value, (String, String)> {
1748 let tasks = initial
1749 .get("tasks")
1750 .and_then(Value::as_array)
1751 .ok_or_else(|| {
1752 (
1753 "States.TaskFailed".to_string(),
1754 "ecs:RunTask.sync: response missing 'tasks' array".to_string(),
1755 )
1756 })?;
1757 if tasks.is_empty() {
1758 return Err((
1759 "States.TaskFailed".to_string(),
1760 "ecs:RunTask.sync: no tasks were started".to_string(),
1761 ));
1762 }
1763 let task_arns: Vec<String> = tasks
1764 .iter()
1765 .filter_map(|t| t.get("taskArn").and_then(Value::as_str).map(String::from))
1766 .collect();
1767 let cluster = input
1768 .get("cluster")
1769 .or_else(|| input.get("Cluster"))
1770 .and_then(Value::as_str)
1771 .map(String::from);
1772
1773 let deadline = sync_deadline(timeout_seconds);
1774 loop {
1775 let mut describe_input = json!({ "tasks": task_arns });
1776 if let Some(c) = &cluster {
1777 describe_input["cluster"] = json!(c);
1778 }
1779 let described = call_sdk_action(
1780 registry,
1781 "ecs",
1782 "DescribeTasks",
1783 &describe_input,
1784 account_id,
1785 )
1786 .await?;
1787 let described_tasks = described
1788 .get("tasks")
1789 .and_then(Value::as_array)
1790 .cloned()
1791 .unwrap_or_default();
1792 let all_stopped = !described_tasks.is_empty()
1793 && described_tasks
1794 .iter()
1795 .all(|t| t.get("lastStatus").and_then(Value::as_str) == Some("STOPPED"));
1796 if all_stopped {
1797 let any_failed = described_tasks.iter().any(|t| {
1802 let stop_code = t.get("stopCode").and_then(Value::as_str);
1803 let bad_stop = matches!(
1804 stop_code,
1805 Some(
1806 "TaskFailedToStart"
1807 | "EssentialContainerExited"
1808 | "ServiceSchedulerInitiated"
1809 )
1810 );
1811 let bad_exit = t
1812 .get("containers")
1813 .and_then(Value::as_array)
1814 .map(|cs| {
1815 cs.iter().any(|c| {
1816 c.get("exitCode")
1817 .and_then(Value::as_i64)
1818 .map(|n| n != 0)
1819 .unwrap_or(false)
1820 })
1821 })
1822 .unwrap_or(false);
1823 bad_stop || bad_exit
1824 });
1825 if any_failed {
1826 let cause = described_tasks
1827 .iter()
1828 .find_map(|t| {
1829 t.get("stoppedReason")
1830 .and_then(Value::as_str)
1831 .map(String::from)
1832 })
1833 .unwrap_or_else(|| "ECS task failed".to_string());
1834 return Err(("States.TaskFailed".to_string(), cause));
1835 }
1836 return Ok(described);
1837 }
1838 if std::time::Instant::now() >= deadline {
1839 return Err((
1840 "States.Timeout".to_string(),
1841 format!(
1842 "ecs:RunTask.sync timed out after {}s waiting for {} task(s) to STOP",
1843 sync_timeout_secs(timeout_seconds),
1844 task_arns.len()
1845 ),
1846 ));
1847 }
1848 tokio::time::sleep(std::time::Duration::from_millis(SYNC_POLL_INTERVAL_MS)).await;
1849 }
1850}
1851
1852async fn sync_wait_athena_query(
1853 registry: &Arc<fakecloud_core::registry::ServiceRegistry>,
1854 initial: &Value,
1855 account_id: &str,
1856 timeout_seconds: Option<u64>,
1857) -> Result<Value, (String, String)> {
1858 let qid = initial
1859 .get("QueryExecutionId")
1860 .and_then(Value::as_str)
1861 .ok_or_else(|| {
1862 (
1863 "States.TaskFailed".to_string(),
1864 "athena:StartQueryExecution.sync: response missing QueryExecutionId".to_string(),
1865 )
1866 })?
1867 .to_string();
1868
1869 let deadline = sync_deadline(timeout_seconds);
1870 loop {
1871 let described = call_sdk_action(
1872 registry,
1873 "athena",
1874 "GetQueryExecution",
1875 &json!({ "QueryExecutionId": qid }),
1876 account_id,
1877 )
1878 .await?;
1879 let state = described
1880 .get("QueryExecution")
1881 .and_then(|qe| qe.get("Status"))
1882 .and_then(|s| s.get("State"))
1883 .and_then(Value::as_str)
1884 .unwrap_or("");
1885 match state {
1886 "SUCCEEDED" => return Ok(described),
1887 "FAILED" | "CANCELLED" => {
1888 let cause = described
1889 .get("QueryExecution")
1890 .and_then(|qe| qe.get("Status"))
1891 .and_then(|s| s.get("StateChangeReason"))
1892 .and_then(Value::as_str)
1893 .unwrap_or("Athena query reached terminal failure state")
1894 .to_string();
1895 return Err(("States.TaskFailed".to_string(), cause));
1896 }
1897 _ => {}
1898 }
1899 if std::time::Instant::now() >= deadline {
1900 return Err((
1901 "States.Timeout".to_string(),
1902 format!(
1903 "athena:StartQueryExecution.sync timed out after {}s for query {qid}",
1904 sync_timeout_secs(timeout_seconds)
1905 ),
1906 ));
1907 }
1908 tokio::time::sleep(std::time::Duration::from_millis(SYNC_POLL_INTERVAL_MS)).await;
1909 }
1910}
1911
1912async fn sync_wait_states_start_execution(
1918 registry: &Arc<fakecloud_core::registry::ServiceRegistry>,
1919 initial: &Value,
1920 account_id: &str,
1921 timeout_seconds: Option<u64>,
1922) -> Result<Value, (String, String)> {
1923 let exec_arn = initial
1924 .get("executionArn")
1925 .or_else(|| initial.get("ExecutionArn"))
1926 .and_then(Value::as_str)
1927 .ok_or_else(|| {
1928 (
1929 "States.TaskFailed".to_string(),
1930 "states:startExecution.sync: response missing executionArn".to_string(),
1931 )
1932 })?
1933 .to_string();
1934
1935 let deadline = sync_deadline(timeout_seconds);
1936 loop {
1937 let described = call_sdk_action(
1938 registry,
1939 "states",
1940 "DescribeExecution",
1941 &json!({ "executionArn": exec_arn }),
1942 account_id,
1943 )
1944 .await?;
1945 let status = described
1946 .get("status")
1947 .or_else(|| described.get("Status"))
1948 .and_then(Value::as_str)
1949 .unwrap_or("");
1950 match status {
1951 "SUCCEEDED" => return Ok(described),
1952 "FAILED" | "TIMED_OUT" | "ABORTED" => {
1953 let cause = described
1954 .get("cause")
1955 .or_else(|| described.get("Cause"))
1956 .and_then(Value::as_str)
1957 .unwrap_or("Nested execution reached terminal failure state")
1958 .to_string();
1959 return Err(("States.TaskFailed".to_string(), cause));
1960 }
1961 _ => {}
1962 }
1963 if std::time::Instant::now() >= deadline {
1964 return Err((
1965 "States.Timeout".to_string(),
1966 format!(
1967 "states:startExecution.sync timed out after {}s for {exec_arn}",
1968 sync_timeout_secs(timeout_seconds)
1969 ),
1970 ));
1971 }
1972 tokio::time::sleep(std::time::Duration::from_millis(SYNC_POLL_INTERVAL_MS)).await;
1973 }
1974}
1975
1976fn sync_timeout_secs(timeout_seconds: Option<u64>) -> u64 {
1977 timeout_seconds.unwrap_or(SYNC_DEFAULT_TIMEOUT_SECS)
1978}
1979
1980fn sync_deadline(timeout_seconds: Option<u64>) -> std::time::Instant {
1981 std::time::Instant::now() + std::time::Duration::from_secs(sync_timeout_secs(timeout_seconds))
1982}
1983
1984#[derive(Clone, Copy)]
1985pub(crate) enum UpdateClause {
1986 Set,
1987 Remove,
1988 Add,
1989 Delete,
1990}
1991
1992async fn invoke_lambda_direct(
1994 function_arn: &str,
1995 input: &Value,
1996 delivery: &Option<Arc<DeliveryBus>>,
1997 timeout_seconds: Option<u64>,
1998) -> Result<Value, (String, String)> {
1999 let delivery = delivery.as_ref().ok_or_else(|| {
2000 (
2001 "States.TaskFailed".to_string(),
2002 "No delivery bus configured for Lambda invocation".to_string(),
2003 )
2004 })?;
2005
2006 let payload =
2007 serde_json::to_string(input).expect("serde_json::Value serialization is infallible");
2008
2009 let invoke_future = delivery.invoke_lambda(function_arn, &payload);
2010
2011 let result = if let Some(timeout) = timeout_seconds {
2012 match tokio::time::timeout(tokio::time::Duration::from_secs(timeout), invoke_future).await {
2013 Ok(r) => r,
2014 Err(_) => {
2015 return Err((
2016 "States.Timeout".to_string(),
2017 format!("Task timed out after {timeout} seconds"),
2018 ));
2019 }
2020 }
2021 } else {
2022 invoke_future.await
2023 };
2024
2025 match result {
2026 Some(Ok(bytes)) => {
2027 let response_str = String::from_utf8_lossy(&bytes);
2028 let value: Value =
2029 serde_json::from_str(&response_str).unwrap_or(json!(response_str.to_string()));
2030
2031 if let Some(obj) = value.as_object() {
2045 if obj.contains_key("errorType") && obj.contains_key("errorMessage") {
2046 let error_type = obj
2047 .get("errorType")
2048 .and_then(Value::as_str)
2049 .unwrap_or("Exception")
2050 .to_string();
2051 let cause = serde_json::to_string(&value)
2052 .expect("serde_json::Value serialization is infallible");
2053 return Err((error_type, cause));
2054 }
2055 }
2056
2057 Ok(value)
2058 }
2059 Some(Err(e)) => {
2060 if e.starts_with("Function not found") {
2070 Err(("Lambda.ResourceNotFoundException".to_string(), e))
2071 } else {
2072 Err(("Lambda.Unknown".to_string(), e))
2073 }
2074 }
2075 None => {
2076 Ok(json!({}))
2078 }
2079 }
2080}
2081
2082async fn invoke_activity(
2087 activity_arn: &str,
2088 input: &Value,
2089 shared_state: &SharedStepFunctionsState,
2090 timeout_seconds: Option<u64>,
2091 heartbeat_seconds: Option<u64>,
2092) -> Result<Value, (String, String)> {
2093 use crate::state::TaskTokenState;
2094
2095 let activity_account = activity_arn.split(':').nth(4).unwrap_or("").to_string();
2097 {
2098 let accounts = shared_state.read();
2099 let exists = accounts
2100 .get(&activity_account)
2101 .map(|s| s.activities.contains_key(activity_arn))
2102 .unwrap_or(false);
2103 if !exists {
2104 return Err((
2105 "States.TaskFailed".to_string(),
2106 format!("Activity does not exist: {activity_arn}"),
2107 ));
2108 }
2109 }
2110
2111 let token = format!(
2112 "FCToken-{}-{}",
2113 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0),
2114 uuid::Uuid::new_v4().simple(),
2115 );
2116 let now = chrono::Utc::now();
2117 let input_str =
2118 serde_json::to_string(input).expect("serde_json::Value serialization is infallible");
2119 {
2120 let mut accounts = shared_state.write();
2121 let state = accounts.get_or_create(&activity_account);
2122 state.task_tokens.insert(
2123 token.clone(),
2124 TaskTokenState {
2125 activity_arn: activity_arn.to_string(),
2126 status: "PENDING".to_string(),
2127 output: None,
2128 error: None,
2129 cause: None,
2130 input: Some(input_str),
2131 created_at: now,
2132 last_heartbeat_at: None,
2133 heartbeat_seconds: heartbeat_seconds.map(|s| s as i64),
2134 timeout_seconds: timeout_seconds.map(|s| s as i64),
2135 },
2136 );
2137 }
2138
2139 poll_task_token(
2140 shared_state,
2141 &activity_account,
2142 &token,
2143 timeout_seconds,
2144 heartbeat_seconds,
2145 )
2146 .await
2147}
2148
2149pub(crate) enum NextState {
2150 Name(String),
2151 End,
2152 Error(String),
2153}
2154
2155#[path = "interpreter_helpers.rs"]
2156mod interpreter_helpers;
2157pub(crate) use interpreter_helpers::*;
2158
2159#[cfg(test)]
2160#[path = "interpreter_tests.rs"]
2161mod tests;