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, create_execution_workflow,
16 read_complete_execution_workflow, read_execution_stage_occupancy, read_execution_workflow,
17 },
18};
19use std::{collections::BTreeMap, future::Future};
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
110pub async fn run_new_execution_workflow<D: ExecutionWorkflowStageDriver>(
129 layout: &BackupLayoutGuard,
130 workflow: &ExecutionWorkflowRecord,
131 driver: &mut D,
132) -> Result<ExecutionWorkflowSettlementView, ExecutionWorkflowRunError<D::Error>> {
133 create_execution_workflow(layout, workflow)?;
134 let mut completed: BTreeMap<u64, ExecutionStagePredecessorRecord> = BTreeMap::new();
135 for node in workflow.allocation().graph().ordered_nodes() {
136 let sequence = node.operation_sequence();
137 let predecessors = node
138 .depends_on()
139 .iter()
140 .map(|dependency| {
141 let original =
142 completed.get(dependency).ok_or(
143 ExecutionWorkflowRunError::<D::Error>::MissingPredecessor(*dependency),
144 )?;
145 let learned = driver
146 .learned_for_successor(original, sequence)
147 .map_err(ExecutionWorkflowRunError::Driver)?;
148 Ok(ExecutionStagePredecessorRecord::new(
149 *dependency,
150 original.binding().clone(),
151 original.settlement().clone(),
152 learned,
153 ))
154 })
155 .collect::<Result<Vec<_>, ExecutionWorkflowRunError<D::Error>>>()?;
156 let plan = driver
157 .bind_stage(workflow, sequence, &predecessors)
158 .map_err(ExecutionWorkflowRunError::Driver)?;
159 let binding = ExecutionStageBindingRecord::new(workflow, sequence, &plan, predecessors)?;
160 let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
161 let learned = driver
162 .execute_stage(&stage)
163 .await
164 .map_err(ExecutionWorkflowRunError::Driver)?;
165 let checkpoint = stage.reconcile_checkpoint(learned)?;
166 completed.insert(sequence, checkpoint);
167 }
168 Ok(read_complete_execution_workflow(
169 layout,
170 &workflow.digest(),
171 )?)
172}
173
174pub async fn run_retained_execution_workflow<D: ExecutionWorkflowStageDriver>(
189 layout: &BackupLayoutGuard,
190 expected_workflow: &ArtifactChecksumRecord,
191 retained: &BTreeMap<u64, RetainedStageEvidence>,
192 driver: &mut D,
193) -> Result<ExecutionWorkflowSettlementView, ExecutionWorkflowRunError<D::Error>> {
194 let workflow = read_execution_workflow(layout, expected_workflow)?;
195 let occupied = read_execution_stage_occupancy(layout, expected_workflow)?;
196 if !retained.keys().copied().eq(occupied.iter().copied()) {
197 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
198 }
199 let mut completed: BTreeMap<u64, ExecutionStagePredecessorRecord> = BTreeMap::new();
200 for (&sequence, evidence) in retained {
201 for successor in evidence.successor_evidence.keys() {
202 let Ok(node) = workflow.allocation().graph().node(*successor) else {
203 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
204 };
205 if !node.depends_on().contains(&sequence) {
206 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
207 }
208 }
209 let stage =
210 ExecutionStageGuard::open(layout, expected_workflow, sequence, &evidence.binding)?;
211 for predecessor in stage.binding().predecessors() {
212 let Some(original) = retained.get(&predecessor.stage_sequence()) else {
213 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
214 };
215 if original.learned_for_successor(sequence) != predecessor.learned_evidence() {
216 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
217 }
218 }
219 let checkpoint = stage.replay_checkpoint(evidence.learned_evidence.clone())?;
220 completed.insert(sequence, checkpoint);
221 }
222 if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
225 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
226 }
227 for node in workflow.allocation().graph().ordered_nodes() {
228 let sequence = node.operation_sequence();
229 if completed.contains_key(&sequence) {
230 continue;
231 }
232 let predecessors = node
233 .depends_on()
234 .iter()
235 .map(|dependency| {
236 let original =
237 completed.get(dependency).ok_or(
238 ExecutionWorkflowRunError::<D::Error>::MissingPredecessor(*dependency),
239 )?;
240 let learned = if let Some(evidence) = retained.get(dependency) {
241 evidence.learned_for_successor(sequence).clone()
242 } else {
243 driver
244 .learned_for_successor(original, sequence)
245 .map_err(ExecutionWorkflowRunError::Driver)?
246 };
247 Ok(ExecutionStagePredecessorRecord::new(
248 *dependency,
249 original.binding().clone(),
250 original.settlement().clone(),
251 learned,
252 ))
253 })
254 .collect::<Result<Vec<_>, ExecutionWorkflowRunError<D::Error>>>()?;
255 let plan = driver
256 .bind_stage(&workflow, sequence, &predecessors)
257 .map_err(ExecutionWorkflowRunError::Driver)?;
258 let binding = ExecutionStageBindingRecord::new(&workflow, sequence, &plan, predecessors)?;
259 let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
260 let learned = driver
261 .execute_stage(&stage)
262 .await
263 .map_err(ExecutionWorkflowRunError::Driver)?;
264 let checkpoint = stage.reconcile_checkpoint(learned)?;
265 completed.insert(sequence, checkpoint);
266 }
267 Ok(read_complete_execution_workflow(layout, expected_workflow)?)
268}
269
270#[derive(Debug, Error)]
272pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
273 #[error("original workflow predecessor {0} did not complete")]
275 MissingPredecessor(u64),
276 #[error("retained stage identities do not match occupied original stages")]
278 RetainedStageSetMismatch,
279 #[error("retained stage {0} has a different learned predecessor commitment")]
282 RetainedLearnedMismatch(u64),
283 #[error(transparent)]
285 Workflow(#[from] ExecutionWorkflowPersistenceError),
286 #[error(transparent)]
288 Binding(#[from] ExecutionWorkflowError),
289 #[error(transparent)]
291 Preparation(#[from] ExecutionStagePreparationError),
292 #[error("original workflow stage driver failed: {0}")]
294 Driver(#[source] E),
295 #[error(transparent)]
297 Checkpoint(#[from] ExecutionStageCheckpointError),
298}
299
300#[cfg(test)]
301mod tests;