1use std::{collections::BTreeMap, fmt, sync::Arc};
2
3use runifold_core::{
4 Checkpoint, CheckpointError, CheckpointErrorKind, CheckpointId, CheckpointStore, RunContext,
5 Usage,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::{StepId, WorkflowError, WorkflowOutcome};
11
12const CHECKPOINT_KIND: &str = "runifold.workflow";
13const CHECKPOINT_SCHEMA_VERSION: u32 = 3;
14
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17#[non_exhaustive]
18pub enum WorkflowResumePolicy {
19 #[default]
21 RejectAmbiguous,
22 RetryInterruptedStep,
24}
25
26#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28#[serde(tag = "state", rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum WorkflowCheckpointPhase {
31 Ready,
33 StepInFlight {
35 step: StepId,
37 },
38 ParallelInFlight {
40 step: StepId,
42 branches: BTreeMap<StepId, ParallelBranchCheckpoint>,
44 },
45 RaceInFlight {
47 step: StepId,
49 branches: BTreeMap<StepId, ParallelBranchCheckpoint>,
51 },
52 Completed {
54 outcome: WorkflowOutcome,
56 },
57}
58
59#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
61#[serde(tag = "state", rename_all = "snake_case")]
62#[non_exhaustive]
63pub enum ParallelBranchCheckpoint {
64 InFlight,
66 Completed {
68 output: Value,
70 },
71 Failed {
73 message: String,
75 },
76 Cancelled,
78}
79
80#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
82pub struct WorkflowCheckpointState {
83 pub workflow: String,
85 pub workflow_version: u32,
87 pub layout: Vec<StepId>,
89 pub next_index: usize,
91 pub value: Value,
93 pub outputs: BTreeMap<StepId, Value>,
95 pub usage: Usage,
97 pub phase: WorkflowCheckpointPhase,
99}
100
101impl WorkflowCheckpointState {
102 pub(crate) fn outcome(&self) -> Option<WorkflowOutcome> {
103 match &self.phase {
104 WorkflowCheckpointPhase::Completed { outcome } => Some(outcome.clone()),
105 _ => None,
106 }
107 }
108}
109
110#[derive(Clone)]
112pub struct WorkflowCheckpoint {
113 id: CheckpointId,
114 store: Arc<dyn CheckpointStore>,
115}
116
117impl WorkflowCheckpoint {
118 pub fn new(store: Arc<dyn CheckpointStore>) -> Self {
120 Self {
121 id: CheckpointId::new(),
122 store,
123 }
124 }
125
126 pub fn existing(id: CheckpointId, store: Arc<dyn CheckpointStore>) -> Self {
128 Self { id, store }
129 }
130
131 pub const fn id(&self) -> CheckpointId {
133 self.id
134 }
135
136 pub fn load(&self) -> Result<(Checkpoint, WorkflowCheckpointState), CheckpointError> {
142 let checkpoint = self.store.load(self.id)?;
143 if checkpoint.kind != CHECKPOINT_KIND
144 || checkpoint.schema_version != CHECKPOINT_SCHEMA_VERSION
145 {
146 return Err(CheckpointError::new(
147 CheckpointErrorKind::InvalidPayload,
148 "checkpoint kind or schema version is not supported",
149 ));
150 }
151 let state = serde_json::from_value(checkpoint.payload.clone()).map_err(|error| {
152 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string())
153 })?;
154 Ok((checkpoint, state))
155 }
156}
157
158impl fmt::Debug for WorkflowCheckpoint {
159 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160 formatter
161 .debug_struct("WorkflowCheckpoint")
162 .field("id", &self.id)
163 .finish_non_exhaustive()
164 }
165}
166
167pub(crate) struct WorkflowCheckpointCursor {
168 handle: WorkflowCheckpoint,
169 envelope: Checkpoint,
170}
171
172impl WorkflowCheckpointCursor {
173 pub(crate) fn create(
174 handle: &WorkflowCheckpoint,
175 run: &RunContext,
176 state: &WorkflowCheckpointState,
177 ) -> Result<Self, WorkflowError> {
178 let envelope = Checkpoint::initial(
179 handle.id,
180 run.run_id(),
181 CHECKPOINT_KIND,
182 CHECKPOINT_SCHEMA_VERSION,
183 serialize(state)?,
184 );
185 handle.store.compare_and_swap(&envelope, None)?;
186 Ok(Self {
187 handle: handle.clone(),
188 envelope,
189 })
190 }
191
192 pub(crate) fn loaded(handle: &WorkflowCheckpoint, envelope: Checkpoint) -> Self {
193 Self {
194 handle: handle.clone(),
195 envelope,
196 }
197 }
198
199 pub(crate) fn save(&mut self, state: &WorkflowCheckpointState) -> Result<(), WorkflowError> {
200 let next = self.envelope.next(serialize(state)?)?;
201 self.handle
202 .store
203 .compare_and_swap(&next, Some(self.envelope.revision))?;
204 self.envelope = next;
205 Ok(())
206 }
207}
208
209fn serialize(state: &WorkflowCheckpointState) -> Result<Value, WorkflowError> {
210 serde_json::to_value(state).map_err(|error| {
211 CheckpointError::new(CheckpointErrorKind::InvalidPayload, error.to_string()).into()
212 })
213}