Skip to main content

ic_backup/workflow/execution_workflow/
mod.rs

1//! Original-workflow stage coordination without a default IC or application adapter.
2
3use 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
22/// Integration-owned exact child planning and one-shot execution of each original stage.
23///
24/// `bind_stage` must qualify learned inputs against the supplied original predecessors.
25/// `execute_stage` owns fresh effect-boundary admission, provider calls, authenticated
26/// request/reply retention and explicit original Applied receipts. Its returned digest
27/// is opaque learned evidence; the coordinator does not authenticate it. Neither
28/// callback may treat local checkpoint admission as a dispatch or release permit.
29pub trait ExecutionWorkflowStageDriver {
30    /// Integration-owned planning, admission or execution error.
31    type Error: std::error::Error + 'static;
32
33    /// Build one exact child plan under the original stage allocation.
34    /// # Errors
35    /// Returns integration-owned learned-input or child-planning rejection.
36    fn bind_stage(
37        &mut self,
38        workflow: &ExecutionWorkflowRecord,
39        stage_sequence: u64,
40        predecessors: &[ExecutionStagePredecessorRecord],
41    ) -> Result<OperationPlanRecord, Self::Error>;
42
43    /// Execute only the already prepared original child; retain original effects.
44    fn execute_stage(
45        &mut self,
46        stage: &ExecutionStageGuard<'_>,
47    ) -> impl Future<Output = Result<ArtifactChecksumRecord, Self::Error>>;
48
49    /// Qualify the learned commitment for one outgoing dependency edge.
50    ///
51    /// The default reuses the stage checkpoint's learned digest. An integration
52    /// may derive a distinct commitment from its retained original evidence;
53    /// this callback must not dispatch effects or change original spending.
54    /// # Errors
55    /// Refuses an unauthenticated or unavailable edge commitment before the
56    /// successor is prepared or its driver callback is invoked.
57    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/// Integration-qualified identities for an already settled original stage.
67///
68/// Local replay derives its binding and settlement from retained records. The
69/// learned digest is supplied by the integration, which must authenticate the
70/// original response and its relation to the settled attempt before resuming.
71#[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    /// Supply an original binding identity and independently qualified learned input.
79    #[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    /// Supply integration-qualified commitments for outgoing edges that differ
92    /// from this stage's checkpoint digest. Keys must name original successors;
93    /// occupied successor bindings are checked before any driver callback.
94    #[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/// Bounded operator advancement, without a second progress or spending ledger.
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub enum ExecutionWorkflowAdvance {
113    /// The selected bound was reached between settled stages; original absent stages remain.
114    Paused,
115    /// Every original stage's exact retained checkpoint passed local replay.
116    /// This grants no product terminal or source-reference release authority.
117    Complete(ExecutionWorkflowSettlementView),
118}
119
120/// Advance at most `stage_limit` absent stages of one prepared original workflow.
121///
122/// Prepare once with `create_execution_workflow`, retaining the original declarations
123/// before effects. Every continuation uses the same complete occupied-stage evidence
124/// admission. A pause happens only between settled stages,
125/// never halfway through a transfer. The limit bounds stage callbacks, not each stage's
126/// original finite effect allowance or elapsed time. All admission, custody, durable
127/// reply retention and effect-specific qualification remain mandatory driver duties.
128///
129/// Pending observations/mutations, partial preparation or changed originals refuse
130/// before any callback; the bound cannot reset them or supply resend permission.
131/// A failed/cancelled callback retains original uncertainty. Inspect it through
132/// `read_execution_workflow_progress` and reconcile through the original owners.
133/// Complete replay invokes no driver callbacks, regardless of the selected limit.
134/// # Errors
135/// Retains the existing typed preparation, checkpoint, original-evidence and driver failures.
136pub 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
152// Each completed stage carries the source of its qualified outgoing edge evidence.
153struct 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    // Recheck the namespace before invoking any integration callback. Create-only
201    // stage preparation still refuses an occupied path after this preflight.
202    if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
203        return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
204    }
205    Ok((workflow, completed))
206}
207
208// Entry points finish their own create-only or retained-evidence admission before
209// sharing dependency propagation and execution of only the remaining stages.
210enum 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/// Failure of new original-workflow coordination; all originals remain retained.
288#[derive(Debug, Error)]
289pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
290    /// A validated predecessor was not available in the current run.
291    #[error("original workflow predecessor {0} did not complete")]
292    MissingPredecessor(u64),
293    /// Supplied identities do not cover exactly the occupied original stage paths.
294    #[error("retained stage identities do not match occupied original stages")]
295    RetainedStageSetMismatch,
296    /// An occupied successor froze a different learned predecessor commitment,
297    /// or supplied edge evidence names a non-successor.
298    #[error("retained stage {0} has a different learned predecessor commitment")]
299    RetainedLearnedMismatch(u64),
300    /// Original allocation could not be published or was already occupied.
301    #[error(transparent)]
302    Workflow(#[from] ExecutionWorkflowPersistenceError),
303    /// A driver-supplied child violated the fixed original allocation.
304    #[error(transparent)]
305    Binding(#[from] ExecutionWorkflowError),
306    /// Complete child preparation failed without replacing original evidence.
307    #[error(transparent)]
308    Preparation(#[from] ExecutionStagePreparationError),
309    /// Driver-owned planning, qualification, provider or receipt step failed.
310    #[error("original workflow stage driver failed: {0}")]
311    Driver(#[source] E),
312    /// Canonical all-Applied checkpoint or final local replay failed.
313    #[error(transparent)]
314    Checkpoint(#[from] ExecutionStageCheckpointError),
315}
316
317#[cfg(test)]
318mod tests;