Skip to main content

fakecloud_stepfunctions/
state.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use fakecloud_aws::arn::Arn;
10
11pub type SharedStepFunctionsState =
12    Arc<RwLock<fakecloud_core::multi_account::MultiRegionState<StepFunctionsState>>>;
13
14impl fakecloud_core::multi_account::AccountState for StepFunctionsState {
15    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
16        Self::new(account_id, region)
17    }
18}
19
20/// v3: state partitioned by (account, region). v2 kept one state per
21/// account; v1 a single account's.
22pub const STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION: u32 = 3;
23
24pub type StepFunctionsSnapshot =
25    fakecloud_core::multi_account::RegionalSnapshot<StepFunctionsState>;
26
27/// Parse a persisted Step Functions snapshot, migrating pre-regional schemas
28/// by moving every resource into the region its ARN names.
29pub fn parse_stepfunctions_snapshot(
30    bytes: &[u8],
31) -> Result<StepFunctionsSnapshot, serde_json::Error> {
32    fakecloud_core::multi_account::parse_regional_snapshot(
33        bytes,
34        STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION,
35        |s: StepFunctionsState| {
36            let (account, region) = (s.account_id.clone(), s.region.clone());
37            fakecloud_core::multi_account::RegionalState::from_legacy(&account, &region, "", s)
38        },
39    )
40}
41
42impl fakecloud_core::multi_account::SplitByRegion for StepFunctionsState {
43    /// State machines, executions, activities, versions, aliases and map
44    /// runs are all keyed by their own ARN and go to its region; an activity
45    /// task token follows its activity (a `.waitForTaskToken` token has none
46    /// and lands in the default region).
47    fn split_by_region(self, into: &mut fakecloud_core::multi_account::RegionalState<Self>) {
48        use fakecloud_aws::arn::region_of;
49        for (k, v) in self.state_machines {
50            into.region_or_default_mut(region_of(&k))
51                .state_machines
52                .insert(k, v);
53        }
54        for (k, v) in self.executions {
55            into.region_or_default_mut(region_of(&k))
56                .executions
57                .insert(k, v);
58        }
59        for (k, v) in self.activities {
60            into.region_or_default_mut(region_of(&k))
61                .activities
62                .insert(k, v);
63        }
64        for (k, v) in self.state_machine_versions {
65            into.region_or_default_mut(region_of(&k))
66                .state_machine_versions
67                .insert(k, v);
68        }
69        for (k, v) in self.state_machine_aliases {
70            into.region_or_default_mut(region_of(&k))
71                .state_machine_aliases
72                .insert(k, v);
73        }
74        for (k, v) in self.map_runs {
75            into.region_or_default_mut(region_of(&k))
76                .map_runs
77                .insert(k, v);
78        }
79        for (k, v) in self.task_tokens {
80            let region = region_of(&v.activity_arn).map(str::to_string);
81            into.region_or_default_mut(region.as_deref())
82                .task_tokens
83                .insert(k, v);
84        }
85    }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct StepFunctionsState {
90    pub account_id: String,
91    pub region: String,
92    /// State machines keyed by ARN.
93    #[serde(default)]
94    pub state_machines: BTreeMap<String, StateMachine>,
95    /// Executions keyed by execution ARN.
96    #[serde(default)]
97    pub executions: BTreeMap<String, Execution>,
98    #[serde(default)]
99    pub activities: BTreeMap<String, Activity>,
100    #[serde(default)]
101    pub state_machine_versions: BTreeMap<String, StateMachineVersion>,
102    #[serde(default)]
103    pub state_machine_aliases: BTreeMap<String, StateMachineAlias>,
104    #[serde(default)]
105    pub map_runs: BTreeMap<String, MapRun>,
106    /// Pending task tokens issued for sync activities + their outcome.
107    #[serde(default)]
108    pub task_tokens: BTreeMap<String, TaskTokenState>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct Activity {
113    pub name: String,
114    pub arn: String,
115    pub creation_date: DateTime<Utc>,
116    pub tags: BTreeMap<String, String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct StateMachineVersion {
121    pub state_machine_arn: String,
122    pub version: i64,
123    pub revision_id: String,
124    pub description: String,
125    pub creation_date: DateTime<Utc>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct StateMachineAlias {
130    pub name: String,
131    pub arn: String,
132    pub description: String,
133    pub routing_configuration: Vec<AliasRoute>,
134    pub creation_date: DateTime<Utc>,
135    pub update_date: DateTime<Utc>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AliasRoute {
140    pub state_machine_version_arn: String,
141    pub weight: i32,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct MapRun {
146    pub map_run_arn: String,
147    pub execution_arn: String,
148    pub max_concurrency: i32,
149    pub tolerated_failure_percentage: f64,
150    pub tolerated_failure_count: i64,
151    pub status: String,
152    pub start_date: DateTime<Utc>,
153    pub stop_date: Option<DateTime<Utc>>,
154    /// Total number of items the distributed Map iterated over.
155    #[serde(default)]
156    pub total_count: i64,
157    /// Iterations that completed successfully.
158    #[serde(default)]
159    pub succeeded_count: i64,
160    /// Iterations that failed.
161    #[serde(default)]
162    pub failed_count: i64,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct TaskTokenState {
167    pub activity_arn: String,
168    /// PENDING (waiting for `GetActivityTask` to dequeue) /
169    /// IN_PROGRESS (worker has picked it up) /
170    /// SUCCEEDED / FAILED / TIMED_OUT.
171    pub status: String,
172    pub output: Option<String>,
173    pub error: Option<String>,
174    pub cause: Option<String>,
175    /// Input the state machine wanted the worker to process. `None`
176    /// for tokens minted by external `GetActivityTask` callers without
177    /// any associated activity execution (legacy synthetic path).
178    #[serde(default)]
179    pub input: Option<String>,
180    #[serde(default = "default_now")]
181    pub created_at: DateTime<Utc>,
182    #[serde(default)]
183    pub last_heartbeat_at: Option<DateTime<Utc>>,
184    /// Per AWS docs: state machine fails the task if no heartbeat in
185    /// this many seconds while the worker is running.
186    #[serde(default)]
187    pub heartbeat_seconds: Option<i64>,
188    /// Overall timeout for the task; counted from `created_at`.
189    #[serde(default)]
190    pub timeout_seconds: Option<i64>,
191}
192
193fn default_now() -> DateTime<Utc> {
194    Utc::now()
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct StateMachine {
199    pub name: String,
200    pub arn: String,
201    pub definition: String,
202    pub role_arn: String,
203    pub machine_type: StateMachineType,
204    pub status: StateMachineStatus,
205    pub creation_date: DateTime<Utc>,
206    pub update_date: DateTime<Utc>,
207    pub tags: BTreeMap<String, String>,
208    pub revision_id: String,
209    pub logging_configuration: Option<Value>,
210    pub tracing_configuration: Option<Value>,
211    pub description: String,
212    #[serde(default)]
213    pub encryption_configuration: Option<Value>,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217pub enum StateMachineType {
218    Standard,
219    Express,
220}
221
222impl StateMachineType {
223    pub fn as_str(&self) -> &'static str {
224        match self {
225            Self::Standard => "STANDARD",
226            Self::Express => "EXPRESS",
227        }
228    }
229
230    pub fn parse(s: &str) -> Option<Self> {
231        match s {
232            "STANDARD" => Some(Self::Standard),
233            "EXPRESS" => Some(Self::Express),
234            _ => None,
235        }
236    }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
240pub enum StateMachineStatus {
241    Active,
242    Deleting,
243}
244
245impl StateMachineStatus {
246    pub fn as_str(&self) -> &'static str {
247        match self {
248            Self::Active => "ACTIVE",
249            Self::Deleting => "DELETING",
250        }
251    }
252}
253
254tokio::task_local! {
255    /// The execution whose Task is calling StartExecution, set only around that
256    /// call. A new execution records it as its `parent_execution_arn`.
257    pub(crate) static STARTED_BY_EXECUTION: String;
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct Execution {
262    pub execution_arn: String,
263    pub state_machine_arn: String,
264    pub state_machine_name: String,
265    pub name: String,
266    pub status: ExecutionStatus,
267    pub input: Option<String>,
268    pub output: Option<String>,
269    pub start_date: DateTime<Utc>,
270    pub stop_date: Option<DateTime<Utc>>,
271    pub error: Option<String>,
272    pub cause: Option<String>,
273    pub history_events: Vec<HistoryEvent>,
274    /// Parent execution ARN when this execution was started by another
275    /// state machine via `arn:aws:states:::states:startExecution[.sync]`.
276    /// `None` for top-level executions started by external callers.
277    #[serde(default)]
278    pub parent_execution_arn: Option<String>,
279    /// True when this execution was created by `StartSyncExecution`
280    /// (EXPRESS state machines only). Distinguishes it from regular
281    /// async executions in introspection endpoints.
282    #[serde(default)]
283    pub is_sync: bool,
284    /// Billed duration in milliseconds, populated on terminal state for
285    /// sync executions. Mirrors `billingDetails.billedDurationInMilliseconds`
286    /// from the StartSyncExecution response.
287    #[serde(default)]
288    pub billed_duration_ms: Option<i64>,
289    /// Billed memory in MB for sync executions. Mirrors
290    /// `billingDetails.billedMemoryUsedInMB`.
291    #[serde(default)]
292    pub billed_memory_mb: Option<i64>,
293    /// The state machine's role when the execution started, recorded on its
294    /// `ExecutionStarted` history event.
295    #[serde(default)]
296    pub role_arn: String,
297    /// How many times `RedriveExecution` has redriven this execution
298    /// (`redriveCount`, `$$.Execution.RedriveCount`). Defaults to 0 for state
299    /// persisted before it was tracked.
300    #[serde(default)]
301    pub redrive_count: u32,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305pub enum ExecutionStatus {
306    Running,
307    Succeeded,
308    Failed,
309    TimedOut,
310    Aborted,
311    PendingRedrive,
312}
313
314impl ExecutionStatus {
315    pub fn as_str(&self) -> &'static str {
316        match self {
317            Self::Running => "RUNNING",
318            Self::Succeeded => "SUCCEEDED",
319            Self::Failed => "FAILED",
320            Self::TimedOut => "TIMED_OUT",
321            Self::Aborted => "ABORTED",
322            Self::PendingRedrive => "PENDING_REDRIVE",
323        }
324    }
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct HistoryEvent {
329    pub id: i64,
330    pub event_type: String,
331    pub timestamp: DateTime<Utc>,
332    pub previous_event_id: i64,
333    pub details: Value,
334}
335
336impl StepFunctionsState {
337    pub fn new(account_id: &str, region: &str) -> Self {
338        Self {
339            account_id: account_id.to_string(),
340            region: region.to_string(),
341            state_machines: BTreeMap::new(),
342            executions: BTreeMap::new(),
343            activities: BTreeMap::new(),
344            state_machine_versions: BTreeMap::new(),
345            state_machine_aliases: BTreeMap::new(),
346            map_runs: BTreeMap::new(),
347            task_tokens: BTreeMap::new(),
348        }
349    }
350
351    pub fn reset(&mut self) {
352        self.state_machines.clear();
353        self.executions.clear();
354        self.activities.clear();
355        self.state_machine_versions.clear();
356        self.state_machine_aliases.clear();
357        self.map_runs.clear();
358        self.task_tokens.clear();
359    }
360
361    pub fn state_machine_arn(&self, region: &str, name: &str) -> String {
362        state_machine_arn(region, &self.account_id, name)
363    }
364
365    pub fn execution_arn(
366        &self,
367        region: &str,
368        state_machine_name: &str,
369        execution_name: &str,
370    ) -> String {
371        execution_arn(region, &self.account_id, state_machine_name, execution_name)
372    }
373
374    /// The ARN for a new execution of a `machine_type` state machine: the
375    /// name-keyed `execution:` ARN for STANDARD, a fresh `express:` ARN for
376    /// EXPRESS (see [`express_execution_arn`]).
377    pub fn new_execution_arn(
378        &self,
379        region: &str,
380        machine_type: StateMachineType,
381        state_machine_name: &str,
382        execution_name: &str,
383    ) -> String {
384        match machine_type {
385            StateMachineType::Standard => {
386                self.execution_arn(region, state_machine_name, execution_name)
387            }
388            StateMachineType::Express => {
389                express_execution_arn(region, &self.account_id, state_machine_name, execution_name)
390            }
391        }
392    }
393}
394
395/// A Step Functions ARN (`arn:<partition>:states:<region>:<account>:<resource>`)
396/// in `region`'s partition.
397pub fn states_arn(region: &str, account_id: &str, resource: &str) -> String {
398    Arn::regional("states", region, account_id, resource).to_string()
399}
400
401/// The ARN of a state machine.
402pub fn state_machine_arn(region: &str, account_id: &str, name: &str) -> String {
403    states_arn(region, account_id, &format!("stateMachine:{name}"))
404}
405
406/// The ARN of a standard-workflow execution.
407pub fn execution_arn(
408    region: &str,
409    account_id: &str,
410    state_machine_name: &str,
411    execution_name: &str,
412) -> String {
413    states_arn(
414        region,
415        account_id,
416        &format!("execution:{state_machine_name}:{execution_name}"),
417    )
418}
419
420/// A fresh ARN for one EXPRESS execution: `express:<machine>:<name>:<id>`.
421///
422/// EXPRESS names can be reused immediately and StartExecution is never
423/// idempotent for them, so every start is its own execution. The trailing id
424/// keeps two starts under the same name apart.
425pub fn express_execution_arn(
426    region: &str,
427    account_id: &str,
428    state_machine_name: &str,
429    execution_name: &str,
430) -> String {
431    states_arn(
432        region,
433        account_id,
434        &format!(
435            "express:{state_machine_name}:{execution_name}:{}",
436            uuid::Uuid::new_v4()
437        ),
438    )
439}
440
441/// The ARN of an activity.
442pub fn activity_arn(region: &str, account_id: &str, name: &str) -> String {
443    states_arn(region, account_id, &format!("activity:{name}"))
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn state_machine_type_as_str() {
452        assert_eq!(StateMachineType::Standard.as_str(), "STANDARD");
453        assert_eq!(StateMachineType::Express.as_str(), "EXPRESS");
454    }
455
456    #[test]
457    fn state_machine_type_parse() {
458        assert_eq!(
459            StateMachineType::parse("STANDARD"),
460            Some(StateMachineType::Standard)
461        );
462        assert_eq!(
463            StateMachineType::parse("EXPRESS"),
464            Some(StateMachineType::Express)
465        );
466        assert_eq!(StateMachineType::parse("bogus"), None);
467    }
468
469    #[test]
470    fn state_machine_status_as_str() {
471        assert_eq!(StateMachineStatus::Active.as_str(), "ACTIVE");
472        assert_eq!(StateMachineStatus::Deleting.as_str(), "DELETING");
473    }
474
475    #[test]
476    fn execution_status_as_str() {
477        assert_eq!(ExecutionStatus::Running.as_str(), "RUNNING");
478        assert_eq!(ExecutionStatus::Succeeded.as_str(), "SUCCEEDED");
479        assert_eq!(ExecutionStatus::Failed.as_str(), "FAILED");
480        assert_eq!(ExecutionStatus::TimedOut.as_str(), "TIMED_OUT");
481        assert_eq!(ExecutionStatus::Aborted.as_str(), "ABORTED");
482        assert_eq!(ExecutionStatus::PendingRedrive.as_str(), "PENDING_REDRIVE");
483    }
484
485    #[test]
486    fn state_machine_arn_format() {
487        let state = StepFunctionsState::new("123456789012", "us-east-1");
488        assert_eq!(
489            state.state_machine_arn("us-east-1", "my-sm"),
490            "arn:aws:states:us-east-1:123456789012:stateMachine:my-sm"
491        );
492    }
493
494    #[test]
495    fn execution_arn_format() {
496        let state = StepFunctionsState::new("123456789012", "us-east-1");
497        assert_eq!(
498            state.execution_arn("us-east-1", "sm", "exec-1"),
499            "arn:aws:states:us-east-1:123456789012:execution:sm:exec-1"
500        );
501    }
502
503    #[test]
504    fn arns_use_request_region_not_server_default() {
505        // Server default region is us-east-1, but a client whose credential
506        // scope resolves to eu-central-1 must get eu-central-1 ARNs.
507        let state = StepFunctionsState::new("123456789012", "us-east-1");
508        assert_eq!(
509            state.state_machine_arn("eu-central-1", "my-sm"),
510            "arn:aws:states:eu-central-1:123456789012:stateMachine:my-sm"
511        );
512        assert_eq!(
513            state.execution_arn("eu-central-1", "sm", "exec-1"),
514            "arn:aws:states:eu-central-1:123456789012:execution:sm:exec-1"
515        );
516    }
517
518    #[test]
519    fn state_reset_clears_all() {
520        let mut state = StepFunctionsState::new("123456789012", "us-east-1");
521        state.state_machines.insert(
522            "x".to_string(),
523            StateMachine {
524                name: "sm".to_string(),
525                arn: "arn:aws:states:us-east-1:123:stateMachine:sm".to_string(),
526                definition: "{}".to_string(),
527                role_arn: "r".to_string(),
528                machine_type: StateMachineType::Standard,
529                status: StateMachineStatus::Active,
530                creation_date: Utc::now(),
531                update_date: Utc::now(),
532                tags: BTreeMap::new(),
533                revision_id: "v1".to_string(),
534                logging_configuration: None,
535                tracing_configuration: None,
536                description: String::new(),
537                encryption_configuration: None,
538            },
539        );
540        state.reset();
541        assert!(state.state_machines.is_empty());
542        assert!(state.executions.is_empty());
543    }
544}