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 {
32 type Error: std::error::Error + 'static;
34
35 fn bind_stage(
39 &mut self,
40 workflow: &ExecutionWorkflowRecord,
41 stage_sequence: u64,
42 predecessors: &[ExecutionStagePredecessorRecord],
43 ) -> Result<OperationPlanRecord, Self::Error>;
44
45 fn execute_stage(
47 &mut self,
48 stage: &ExecutionStageGuard<'_>,
49 ) -> impl Future<Output = Result<ArtifactChecksumRecord, Self::Error>>;
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct RetainedStageEvidence {
59 binding: ArtifactChecksumRecord,
60 learned_evidence: ArtifactChecksumRecord,
61}
62impl RetainedStageEvidence {
63 #[must_use]
65 pub const fn learned_evidence(&self) -> &ArtifactChecksumRecord {
66 &self.learned_evidence
67 }
68
69 #[must_use]
71 pub const fn new(
72 binding: ArtifactChecksumRecord,
73 learned_evidence: ArtifactChecksumRecord,
74 ) -> Self {
75 Self {
76 binding,
77 learned_evidence,
78 }
79 }
80}
81
82#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum ExecutionWorkflowAdvance {
85 Paused,
87 Complete(ExecutionWorkflowSettlementView),
90}
91
92pub async fn advance_execution_workflow<D: ExecutionWorkflowStageDriver>(
109 layout: &BackupLayoutGuard,
110 expected_workflow: &ArtifactChecksumRecord,
111 retained: &BTreeMap<u64, RetainedStageEvidence>,
112 stage_limit: NonZeroUsize,
113 driver: &mut D,
114) -> Result<ExecutionWorkflowAdvance, ExecutionWorkflowRunError<D::Error>> {
115 let (workflow, mut completed) = admit_retained_stages(layout, expected_workflow, retained)?;
116 let mut advanced = 0;
117 for node in workflow.allocation().ordered_operations() {
118 let sequence = node.operation_sequence();
119 if completed.contains_key(&sequence) {
120 continue;
121 }
122 if advanced == stage_limit.get() {
123 return Ok(ExecutionWorkflowAdvance::Paused);
124 }
125 let predecessors = node
126 .depends_on()
127 .iter()
128 .map(|dependency| {
129 completed.get(dependency).cloned().ok_or(
130 ExecutionWorkflowRunError::<D::Error>::MissingPredecessor(*dependency),
131 )
132 })
133 .collect::<Result<Vec<_>, ExecutionWorkflowRunError<D::Error>>>()?;
134 let checkpoint =
135 execute_absent_stage(layout, &workflow, sequence, predecessors, driver).await?;
136 completed.insert(sequence, checkpoint);
137 advanced += 1;
138 }
139 Ok(ExecutionWorkflowAdvance::Complete(
140 read_complete_execution_workflow(layout, expected_workflow)?,
141 ))
142}
143
144fn admit_retained_stages<E: std::error::Error + 'static>(
145 layout: &BackupLayoutGuard,
146 expected_workflow: &ArtifactChecksumRecord,
147 retained: &BTreeMap<u64, RetainedStageEvidence>,
148) -> Result<
149 (
150 ExecutionWorkflowRecord,
151 BTreeMap<u64, ExecutionStagePredecessorRecord>,
152 ),
153 ExecutionWorkflowRunError<E>,
154> {
155 let workflow = read_execution_workflow(layout, expected_workflow)?;
156 let occupied = read_execution_stage_occupancy(layout, expected_workflow)?;
157 if !retained.keys().copied().eq(occupied.iter().copied()) {
158 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
159 }
160 let mut completed = BTreeMap::new();
161 for (&sequence, evidence) in retained {
162 let stage =
163 ExecutionStageGuard::open(layout, expected_workflow, sequence, &evidence.binding)?;
164 for predecessor in stage.binding().predecessors() {
165 let Some(original) = retained.get(&predecessor.stage_sequence()) else {
166 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
167 };
168 if &original.learned_evidence != predecessor.learned_evidence() {
169 return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
170 }
171 }
172 let checkpoint = stage.replay_checkpoint(evidence.learned_evidence.clone())?;
173 completed.insert(sequence, checkpoint);
174 }
175 if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
178 return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
179 }
180 Ok((workflow, completed))
181}
182
183async fn execute_absent_stage<D: ExecutionWorkflowStageDriver>(
184 layout: &BackupLayoutGuard,
185 workflow: &ExecutionWorkflowRecord,
186 sequence: u64,
187 predecessors: Vec<ExecutionStagePredecessorRecord>,
188 driver: &mut D,
189) -> Result<ExecutionStagePredecessorRecord, ExecutionWorkflowRunError<D::Error>> {
190 let plan = driver
191 .bind_stage(workflow, sequence, &predecessors)
192 .map_err(ExecutionWorkflowRunError::Driver)?;
193 let binding = ExecutionStageBindingRecord::new(workflow, sequence, &plan, predecessors)?;
194 let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
195 let learned = driver
196 .execute_stage(&stage)
197 .await
198 .map_err(ExecutionWorkflowRunError::Driver)?;
199 Ok(stage.reconcile_checkpoint(learned)?)
200}
201
202#[derive(Debug, Error)]
204pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
205 #[error("original workflow predecessor {0} did not complete")]
207 MissingPredecessor(u64),
208 #[error("retained stage identities do not match occupied original stages")]
210 RetainedStageSetMismatch,
211 #[error("retained stage {0} has a different learned predecessor commitment")]
213 RetainedLearnedMismatch(u64),
214 #[error(transparent)]
216 Workflow(#[from] ExecutionWorkflowPersistenceError),
217 #[error(transparent)]
219 Binding(#[from] ExecutionWorkflowError),
220 #[error(transparent)]
222 Preparation(#[from] ExecutionStagePreparationError),
223 #[error("original workflow stage driver failed: {0}")]
225 Driver(#[source] E),
226 #[error(transparent)]
228 Checkpoint(#[from] ExecutionStageCheckpointError),
229}
230
231#[cfg(test)]
232mod tests;