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 checkpoint =
157 execute_absent_stage(layout, workflow, sequence, predecessors, driver).await?;
158 completed.insert(sequence, checkpoint);
159 }
160 Ok(read_complete_execution_workflow(
161 layout,
162 &workflow.digest(),
163 )?)
164}
165
166pub async fn run_retained_execution_workflow<D: ExecutionWorkflowStageDriver>(
181 layout: &BackupLayoutGuard,
182 expected_workflow: &ArtifactChecksumRecord,
183 retained: &BTreeMap<u64, RetainedStageEvidence>,
184 driver: &mut D,
185) -> Result<ExecutionWorkflowSettlementView, ExecutionWorkflowRunError<D::Error>> {
186 let workflow = read_execution_workflow(layout, expected_workflow)?;
187 let occupied = read_execution_stage_occupancy(layout, expected_workflow)?;
188 if !retained.keys().copied().eq(occupied.iter().copied()) {
189 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
190 }
191 let mut completed: BTreeMap<u64, ExecutionStagePredecessorRecord> = BTreeMap::new();
192 for (&sequence, evidence) in retained {
193 for successor in evidence.successor_evidence.keys() {
194 let Ok(node) = workflow.allocation().graph().node(*successor) else {
195 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
196 };
197 if !node.depends_on().contains(&sequence) {
198 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
199 }
200 }
201 let stage =
202 ExecutionStageGuard::open(layout, expected_workflow, sequence, &evidence.binding)?;
203 for predecessor in stage.binding().predecessors() {
204 let Some(original) = retained.get(&predecessor.stage_sequence()) else {
205 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
206 };
207 if original.learned_for_successor(sequence) != predecessor.learned_evidence() {
208 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
209 }
210 }
211 let checkpoint = stage.replay_checkpoint(evidence.learned_evidence.clone())?;
212 completed.insert(sequence, checkpoint);
213 }
214 if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
217 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
218 }
219 for node in workflow.allocation().graph().ordered_nodes() {
220 let sequence = node.operation_sequence();
221 if completed.contains_key(&sequence) {
222 continue;
223 }
224 let predecessors = node
225 .depends_on()
226 .iter()
227 .map(|dependency| {
228 let original =
229 completed.get(dependency).ok_or(
230 ExecutionWorkflowRunError::<D::Error>::MissingPredecessor(*dependency),
231 )?;
232 let learned = if let Some(evidence) = retained.get(dependency) {
233 evidence.learned_for_successor(sequence).clone()
234 } else {
235 driver
236 .learned_for_successor(original, sequence)
237 .map_err(ExecutionWorkflowRunError::Driver)?
238 };
239 Ok(ExecutionStagePredecessorRecord::new(
240 *dependency,
241 original.binding().clone(),
242 original.settlement().clone(),
243 learned,
244 ))
245 })
246 .collect::<Result<Vec<_>, ExecutionWorkflowRunError<D::Error>>>()?;
247 let checkpoint =
248 execute_absent_stage(layout, &workflow, sequence, predecessors, driver).await?;
249 completed.insert(sequence, checkpoint);
250 }
251 Ok(read_complete_execution_workflow(layout, expected_workflow)?)
252}
253
254async fn execute_absent_stage<D: ExecutionWorkflowStageDriver>(
255 layout: &BackupLayoutGuard,
256 workflow: &ExecutionWorkflowRecord,
257 sequence: u64,
258 predecessors: Vec<ExecutionStagePredecessorRecord>,
259 driver: &mut D,
260) -> Result<ExecutionStagePredecessorRecord, ExecutionWorkflowRunError<D::Error>> {
261 let plan = driver
262 .bind_stage(workflow, sequence, &predecessors)
263 .map_err(ExecutionWorkflowRunError::Driver)?;
264 let binding = ExecutionStageBindingRecord::new(workflow, sequence, &plan, predecessors)?;
265 let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
266 let learned = driver
267 .execute_stage(&stage)
268 .await
269 .map_err(ExecutionWorkflowRunError::Driver)?;
270 Ok(stage.reconcile_checkpoint(learned)?)
271}
272
273#[derive(Debug, Error)]
275pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
276 #[error("original workflow predecessor {0} did not complete")]
278 MissingPredecessor(u64),
279 #[error("retained stage identities do not match occupied original stages")]
281 RetainedStageSetMismatch,
282 #[error("retained stage {0} has a different learned predecessor commitment")]
285 RetainedLearnedMismatch(u64),
286 #[error(transparent)]
288 Workflow(#[from] ExecutionWorkflowPersistenceError),
289 #[error(transparent)]
291 Binding(#[from] ExecutionWorkflowError),
292 #[error(transparent)]
294 Preparation(#[from] ExecutionStagePreparationError),
295 #[error("original workflow stage driver failed: {0}")]
297 Driver(#[source] E),
298 #[error(transparent)]
300 Checkpoint(#[from] ExecutionStageCheckpointError),
301}
302
303#[cfg(test)]
304mod tests;