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 fn learned_for_successor(
58 &mut self,
59 predecessor: &ExecutionStagePredecessorRecord,
60 _successor_sequence: u64,
61 ) -> Result<ArtifactChecksumRecord, Self::Error> {
62 Ok(predecessor.learned_evidence().clone())
63 }
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct RetainedStageEvidence {
73 binding: ArtifactChecksumRecord,
74 learned_evidence: ArtifactChecksumRecord,
75 successor_evidence: BTreeMap<u64, ArtifactChecksumRecord>,
76}
77impl RetainedStageEvidence {
78 #[must_use]
80 pub const fn new(
81 binding: ArtifactChecksumRecord,
82 learned_evidence: ArtifactChecksumRecord,
83 ) -> Self {
84 Self {
85 binding,
86 learned_evidence,
87 successor_evidence: BTreeMap::new(),
88 }
89 }
90
91 #[must_use]
95 pub fn with_successor_evidence(
96 mut self,
97 successor_evidence: BTreeMap<u64, ArtifactChecksumRecord>,
98 ) -> Self {
99 self.successor_evidence = successor_evidence;
100 self
101 }
102
103 fn learned_for_successor(&self, sequence: u64) -> &ArtifactChecksumRecord {
104 self.successor_evidence
105 .get(&sequence)
106 .unwrap_or(&self.learned_evidence)
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
112pub enum ExecutionWorkflowAdvance {
113 Paused,
115 Complete(ExecutionWorkflowSettlementView),
118}
119
120pub async fn advance_execution_workflow<D: ExecutionWorkflowStageDriver>(
137 layout: &BackupLayoutGuard,
138 expected_workflow: &ArtifactChecksumRecord,
139 retained: &BTreeMap<u64, RetainedStageEvidence>,
140 stage_limit: NonZeroUsize,
141 driver: &mut D,
142) -> Result<ExecutionWorkflowAdvance, ExecutionWorkflowRunError<D::Error>> {
143 let (workflow, completed) = admit_retained_stages(layout, expected_workflow, retained)?;
144 match execute_remaining_stages(layout, &workflow, completed, stage_limit.get(), driver).await? {
145 StageExecutionOutcome::Complete => Ok(ExecutionWorkflowAdvance::Complete(
146 read_complete_execution_workflow(layout, expected_workflow)?,
147 )),
148 StageExecutionOutcome::Paused => Ok(ExecutionWorkflowAdvance::Paused),
149 }
150}
151
152struct CompletedStage<'a> {
154 checkpoint: ExecutionStagePredecessorRecord,
155 retained: Option<&'a RetainedStageEvidence>,
156}
157
158fn admit_retained_stages<'a, E: std::error::Error + 'static>(
159 layout: &BackupLayoutGuard,
160 expected_workflow: &ArtifactChecksumRecord,
161 retained: &'a BTreeMap<u64, RetainedStageEvidence>,
162) -> Result<
163 (ExecutionWorkflowRecord, BTreeMap<u64, CompletedStage<'a>>),
164 ExecutionWorkflowRunError<E>,
165> {
166 let workflow = read_execution_workflow(layout, expected_workflow)?;
167 let occupied = read_execution_stage_occupancy(layout, expected_workflow)?;
168 if !retained.keys().copied().eq(occupied.iter().copied()) {
169 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
170 }
171 let mut completed = BTreeMap::new();
172 for (&sequence, evidence) in retained {
173 for successor in evidence.successor_evidence.keys() {
174 let Ok(node) = workflow.allocation().graph().node(*successor) else {
175 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
176 };
177 if !node.depends_on().contains(&sequence) {
178 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
179 }
180 }
181 let stage =
182 ExecutionStageGuard::open(layout, expected_workflow, sequence, &evidence.binding)?;
183 for predecessor in stage.binding().predecessors() {
184 let Some(original) = retained.get(&predecessor.stage_sequence()) else {
185 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
186 };
187 if original.learned_for_successor(sequence) != predecessor.learned_evidence() {
188 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
189 }
190 }
191 let checkpoint = stage.replay_checkpoint(evidence.learned_evidence.clone())?;
192 completed.insert(
193 sequence,
194 CompletedStage {
195 checkpoint,
196 retained: Some(evidence),
197 },
198 );
199 }
200 if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
203 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
204 }
205 Ok((workflow, completed))
206}
207
208enum StageExecutionOutcome {
211 Complete,
212 Paused,
213}
214
215async fn execute_remaining_stages<D: ExecutionWorkflowStageDriver>(
216 layout: &BackupLayoutGuard,
217 workflow: &ExecutionWorkflowRecord,
218 mut completed: BTreeMap<u64, CompletedStage<'_>>,
219 stage_limit: usize,
220 driver: &mut D,
221) -> Result<StageExecutionOutcome, ExecutionWorkflowRunError<D::Error>> {
222 let mut advanced = 0;
223 for node in workflow.allocation().graph().ordered_nodes() {
224 let sequence = node.operation_sequence();
225 if completed.contains_key(&sequence) {
226 continue;
227 }
228 if advanced == stage_limit {
229 return Ok(StageExecutionOutcome::Paused);
230 }
231 let predecessors = node
232 .depends_on()
233 .iter()
234 .map(|dependency| {
235 let original =
236 completed.get(dependency).ok_or(
237 ExecutionWorkflowRunError::<D::Error>::MissingPredecessor(*dependency),
238 )?;
239 let learned = if let Some(evidence) = original.retained {
240 evidence.learned_for_successor(sequence).clone()
241 } else {
242 driver
243 .learned_for_successor(&original.checkpoint, sequence)
244 .map_err(ExecutionWorkflowRunError::Driver)?
245 };
246 Ok(ExecutionStagePredecessorRecord::new(
247 *dependency,
248 original.checkpoint.binding().clone(),
249 original.checkpoint.settlement().clone(),
250 learned,
251 ))
252 })
253 .collect::<Result<Vec<_>, ExecutionWorkflowRunError<D::Error>>>()?;
254 let checkpoint =
255 execute_absent_stage(layout, workflow, sequence, predecessors, driver).await?;
256 completed.insert(
257 sequence,
258 CompletedStage {
259 checkpoint,
260 retained: None,
261 },
262 );
263 advanced += 1;
264 }
265 Ok(StageExecutionOutcome::Complete)
266}
267
268async fn execute_absent_stage<D: ExecutionWorkflowStageDriver>(
269 layout: &BackupLayoutGuard,
270 workflow: &ExecutionWorkflowRecord,
271 sequence: u64,
272 predecessors: Vec<ExecutionStagePredecessorRecord>,
273 driver: &mut D,
274) -> Result<ExecutionStagePredecessorRecord, ExecutionWorkflowRunError<D::Error>> {
275 let plan = driver
276 .bind_stage(workflow, sequence, &predecessors)
277 .map_err(ExecutionWorkflowRunError::Driver)?;
278 let binding = ExecutionStageBindingRecord::new(workflow, sequence, &plan, predecessors)?;
279 let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
280 let learned = driver
281 .execute_stage(&stage)
282 .await
283 .map_err(ExecutionWorkflowRunError::Driver)?;
284 Ok(stage.reconcile_checkpoint(learned)?)
285}
286
287#[derive(Debug, Error)]
289pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
290 #[error("original workflow predecessor {0} did not complete")]
292 MissingPredecessor(u64),
293 #[error("retained stage identities do not match occupied original stages")]
295 RetainedStageSetMismatch,
296 #[error("retained stage {0} has a different learned predecessor commitment")]
299 RetainedLearnedMismatch(u64),
300 #[error(transparent)]
302 Workflow(#[from] ExecutionWorkflowPersistenceError),
303 #[error(transparent)]
305 Binding(#[from] ExecutionWorkflowError),
306 #[error(transparent)]
308 Preparation(#[from] ExecutionStagePreparationError),
309 #[error("original workflow stage driver failed: {0}")]
311 Driver(#[source] E),
312 #[error(transparent)]
314 Checkpoint(#[from] ExecutionStageCheckpointError),
315}
316
317#[cfg(test)]
318mod tests;