ic_backup/workflow/execution_workflow/
mod.rs1use crate::{
4 model::{
5 artifacts::ArtifactChecksumRecord,
6 execution_workflow::{
7 ExecutionStageBindingRecord, ExecutionStagePredecessorRecord, ExecutionWorkflowError,
8 ExecutionWorkflowRecord,
9 },
10 operation_plan::OperationPlanRecord,
11 },
12 ops::persistence::{
13 BackupLayoutGuard, ExecutionStageCheckpointError, ExecutionStageGuard,
14 ExecutionStagePreparationError, ExecutionWorkflowPersistenceError,
15 ExecutionWorkflowSettlementView, read_complete_execution_workflow,
16 read_execution_stage_occupancy, read_execution_workflow,
17 },
18};
19use std::{collections::BTreeMap, future::Future, num::NonZeroUsize};
20use thiserror::Error;
21
22pub trait ExecutionWorkflowStageDriver {
30 type Error: std::error::Error + 'static;
32
33 fn bind_stage(
37 &mut self,
38 workflow: &ExecutionWorkflowRecord,
39 stage_sequence: u64,
40 predecessors: &[ExecutionStagePredecessorRecord],
41 ) -> Result<OperationPlanRecord, Self::Error>;
42
43 fn execute_stage(
45 &mut self,
46 stage: &ExecutionStageGuard<'_>,
47 ) -> impl Future<Output = Result<ArtifactChecksumRecord, Self::Error>>;
48}
49
50#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct RetainedStageEvidence {
57 binding: ArtifactChecksumRecord,
58 learned_evidence: ArtifactChecksumRecord,
59}
60impl RetainedStageEvidence {
61 #[must_use]
63 pub const fn new(
64 binding: ArtifactChecksumRecord,
65 learned_evidence: ArtifactChecksumRecord,
66 ) -> Self {
67 Self {
68 binding,
69 learned_evidence,
70 }
71 }
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
76pub enum ExecutionWorkflowAdvance {
77 Paused,
79 Complete(ExecutionWorkflowSettlementView),
82}
83
84pub async fn advance_execution_workflow<D: ExecutionWorkflowStageDriver>(
101 layout: &BackupLayoutGuard,
102 expected_workflow: &ArtifactChecksumRecord,
103 retained: &BTreeMap<u64, RetainedStageEvidence>,
104 stage_limit: NonZeroUsize,
105 driver: &mut D,
106) -> Result<ExecutionWorkflowAdvance, ExecutionWorkflowRunError<D::Error>> {
107 let (workflow, completed) = admit_retained_stages(layout, expected_workflow, retained)?;
108 match execute_remaining_stages(layout, &workflow, completed, stage_limit.get(), driver).await? {
109 StageExecutionOutcome::Complete => Ok(ExecutionWorkflowAdvance::Complete(
110 read_complete_execution_workflow(layout, expected_workflow)?,
111 )),
112 StageExecutionOutcome::Paused => Ok(ExecutionWorkflowAdvance::Paused),
113 }
114}
115
116fn admit_retained_stages<E: std::error::Error + 'static>(
117 layout: &BackupLayoutGuard,
118 expected_workflow: &ArtifactChecksumRecord,
119 retained: &BTreeMap<u64, RetainedStageEvidence>,
120) -> Result<
121 (
122 ExecutionWorkflowRecord,
123 BTreeMap<u64, ExecutionStagePredecessorRecord>,
124 ),
125 ExecutionWorkflowRunError<E>,
126> {
127 let workflow = read_execution_workflow(layout, expected_workflow)?;
128 let occupied = read_execution_stage_occupancy(layout, expected_workflow)?;
129 if !retained.keys().copied().eq(occupied.iter().copied()) {
130 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
131 }
132 let mut completed = BTreeMap::new();
133 for (&sequence, evidence) in retained {
134 let stage =
135 ExecutionStageGuard::open(layout, expected_workflow, sequence, &evidence.binding)?;
136 for predecessor in stage.binding().predecessors() {
137 let Some(original) = retained.get(&predecessor.stage_sequence()) else {
138 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
139 };
140 if &original.learned_evidence != predecessor.learned_evidence() {
141 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
142 }
143 }
144 let checkpoint = stage.replay_checkpoint(evidence.learned_evidence.clone())?;
145 completed.insert(sequence, checkpoint);
146 }
147 if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
150 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
151 }
152 Ok((workflow, completed))
153}
154
155enum StageExecutionOutcome {
158 Complete,
159 Paused,
160}
161
162async fn execute_remaining_stages<D: ExecutionWorkflowStageDriver>(
163 layout: &BackupLayoutGuard,
164 workflow: &ExecutionWorkflowRecord,
165 mut completed: BTreeMap<u64, ExecutionStagePredecessorRecord>,
166 stage_limit: usize,
167 driver: &mut D,
168) -> Result<StageExecutionOutcome, ExecutionWorkflowRunError<D::Error>> {
169 let mut advanced = 0;
170 for node in workflow.allocation().graph().ordered_nodes() {
171 let sequence = node.operation_sequence();
172 if completed.contains_key(&sequence) {
173 continue;
174 }
175 if advanced == stage_limit {
176 return Ok(StageExecutionOutcome::Paused);
177 }
178 let predecessors = node
179 .depends_on()
180 .iter()
181 .map(|dependency| {
182 let original =
183 completed.get(dependency).ok_or(
184 ExecutionWorkflowRunError::<D::Error>::MissingPredecessor(*dependency),
185 )?;
186 Ok(ExecutionStagePredecessorRecord::new(
187 *dependency,
188 original.binding().clone(),
189 original.settlement().clone(),
190 original.learned_evidence().clone(),
191 ))
192 })
193 .collect::<Result<Vec<_>, ExecutionWorkflowRunError<D::Error>>>()?;
194 let checkpoint =
195 execute_absent_stage(layout, workflow, sequence, predecessors, driver).await?;
196 completed.insert(sequence, checkpoint);
197 advanced += 1;
198 }
199 Ok(StageExecutionOutcome::Complete)
200}
201
202async fn execute_absent_stage<D: ExecutionWorkflowStageDriver>(
203 layout: &BackupLayoutGuard,
204 workflow: &ExecutionWorkflowRecord,
205 sequence: u64,
206 predecessors: Vec<ExecutionStagePredecessorRecord>,
207 driver: &mut D,
208) -> Result<ExecutionStagePredecessorRecord, ExecutionWorkflowRunError<D::Error>> {
209 let plan = driver
210 .bind_stage(workflow, sequence, &predecessors)
211 .map_err(ExecutionWorkflowRunError::Driver)?;
212 let binding = ExecutionStageBindingRecord::new(workflow, sequence, &plan, predecessors)?;
213 let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
214 let learned = driver
215 .execute_stage(&stage)
216 .await
217 .map_err(ExecutionWorkflowRunError::Driver)?;
218 Ok(stage.reconcile_checkpoint(learned)?)
219}
220
221#[derive(Debug, Error)]
223pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
224 #[error("original workflow predecessor {0} did not complete")]
226 MissingPredecessor(u64),
227 #[error("retained stage identities do not match occupied original stages")]
229 RetainedStageSetMismatch,
230 #[error("retained stage {0} has a different learned predecessor commitment")]
232 RetainedLearnedMismatch(u64),
233 #[error(transparent)]
235 Workflow(#[from] ExecutionWorkflowPersistenceError),
236 #[error(transparent)]
238 Binding(#[from] ExecutionWorkflowError),
239 #[error(transparent)]
241 Preparation(#[from] ExecutionStagePreparationError),
242 #[error("original workflow stage driver failed: {0}")]
244 Driver(#[source] E),
245 #[error(transparent)]
247 Checkpoint(#[from] ExecutionStageCheckpointError),
248}
249
250#[cfg(test)]
251mod tests;