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, 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
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
50/// Integration-qualified identities for an already settled original stage.
51///
52/// Local replay derives its binding and settlement from retained records. The
53/// learned digest is supplied by the integration, which must authenticate the
54/// original response and its relation to the settled attempt before resuming.
55#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct RetainedStageEvidence {
57    binding: ArtifactChecksumRecord,
58    learned_evidence: ArtifactChecksumRecord,
59}
60impl RetainedStageEvidence {
61    /// Supply an original binding identity and independently qualified learned input.
62    #[must_use]
63    pub const fn new(
64        binding: ArtifactChecksumRecord,
65        learned_evidence: ArtifactChecksumRecord,
66    ) -> Self {
67        Self {
68            binding,
69            learned_evidence,
70        }
71    }
72}
73
74/// Coordinate one *new* fixed workflow in its declared dependency order.
75///
76/// Publish the original allocation once, prepare each child and all its original
77/// journals before invoking the driver, then reconcile its exact all-Applied
78/// checkpoint. The graph's topological order controls stage dependencies, never
79/// numeric sequence order. Success reads back every retained checkpoint through
80/// the canonical complete-workflow owner. No automatic outcome, retry, refund,
81/// terminal receipt, fence or reference release is inferred.
82///
83/// This is a deliberately one-shot entry point. A failure or cancellation retains
84/// the original workflow, prepared stages, pending spending, artifacts and all
85/// obligations. Calling it again refuses the occupied workflow *before* invoking
86/// the driver. Recovery must reopen and reconcile those exact originals through
87/// their separate owners. Retained continuation requires a separately qualified
88/// complete occupied-stage set; a new run cannot reconstruct authority from a prefix.
89/// # Errors
90/// Rejects occupied or changed originals, incomplete stages and driver failures,
91/// preserving all retained evidence without reissuing an effect.
92pub async fn run_new_execution_workflow<D: ExecutionWorkflowStageDriver>(
93    layout: &BackupLayoutGuard,
94    workflow: &ExecutionWorkflowRecord,
95    driver: &mut D,
96) -> Result<ExecutionWorkflowSettlementView, ExecutionWorkflowRunError<D::Error>> {
97    create_execution_workflow(layout, workflow)?;
98    let mut completed: BTreeMap<u64, ExecutionStagePredecessorRecord> = BTreeMap::new();
99    for node in workflow.allocation().graph().ordered_nodes() {
100        let sequence = node.operation_sequence();
101        let predecessors = node
102            .depends_on()
103            .iter()
104            .map(|dependency| {
105                completed
106                    .get(dependency)
107                    .cloned()
108                    .ok_or(ExecutionWorkflowRunError::MissingPredecessor(*dependency))
109            })
110            .collect::<Result<Vec<_>, _>>()?;
111        let plan = driver
112            .bind_stage(workflow, sequence, &predecessors)
113            .map_err(ExecutionWorkflowRunError::Driver)?;
114        let binding = ExecutionStageBindingRecord::new(workflow, sequence, &plan, predecessors)?;
115        let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
116        let learned = driver
117            .execute_stage(&stage)
118            .await
119            .map_err(ExecutionWorkflowRunError::Driver)?;
120        let checkpoint = stage.reconcile_checkpoint(learned)?;
121        completed.insert(sequence, checkpoint);
122    }
123    Ok(read_complete_execution_workflow(
124        layout,
125        &workflow.digest(),
126    )?)
127}
128
129/// Continue an existing allocation after replaying every occupied settled stage.
130///
131/// The caller supplies the complete occupied stage set and authenticates each
132/// stage's learned evidence. Before any driver callback, the coordinator checks
133/// that set against the held layout and replays every exact all-Applied original
134/// checkpoint. Already bound successor edges must commit the same learned digest
135/// supplied for their occupied predecessor. This coordinator has one learned
136/// digest per stage; edge-specific learned identities require the lower-level
137/// stage owners. Pending, partially prepared or omitted occupied stages refuse;
138/// none is reissued. Only absent stages may be prepared and executed once, with
139/// fresh authority and external-effect admission remaining with the driver.
140/// This is no terminal receipt, same-ID restore or reference-release permit.
141/// # Errors
142/// Rejects incomplete or changed retained evidence before new driver callbacks;
143/// later stage failures preserve every original journal and effect obligation.
144pub async fn run_retained_execution_workflow<D: ExecutionWorkflowStageDriver>(
145    layout: &BackupLayoutGuard,
146    expected_workflow: &ArtifactChecksumRecord,
147    retained: &BTreeMap<u64, RetainedStageEvidence>,
148    driver: &mut D,
149) -> Result<ExecutionWorkflowSettlementView, ExecutionWorkflowRunError<D::Error>> {
150    let workflow = read_execution_workflow(layout, expected_workflow)?;
151    let occupied = read_execution_stage_occupancy(layout, expected_workflow)?;
152    if !retained.keys().copied().eq(occupied.iter().copied()) {
153        return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
154    }
155    let mut completed: BTreeMap<u64, ExecutionStagePredecessorRecord> = BTreeMap::new();
156    for (&sequence, evidence) in retained {
157        let stage =
158            ExecutionStageGuard::open(layout, expected_workflow, sequence, &evidence.binding)?;
159        for predecessor in stage.binding().predecessors() {
160            let Some(original) = retained.get(&predecessor.stage_sequence()) else {
161                return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
162            };
163            if &original.learned_evidence != predecessor.learned_evidence() {
164                return Err(ExecutionWorkflowRunError::RetainedLearnedMismatch(sequence));
165            }
166        }
167        let checkpoint = stage.replay_checkpoint(evidence.learned_evidence.clone())?;
168        completed.insert(sequence, checkpoint);
169    }
170    // Recheck the namespace before invoking any integration callback. Create-only
171    // stage preparation still refuses an occupied path after this preflight.
172    if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
173        return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
174    }
175    for node in workflow.allocation().graph().ordered_nodes() {
176        let sequence = node.operation_sequence();
177        if completed.contains_key(&sequence) {
178            continue;
179        }
180        let predecessors = node
181            .depends_on()
182            .iter()
183            .map(|dependency| {
184                completed
185                    .get(dependency)
186                    .cloned()
187                    .ok_or(ExecutionWorkflowRunError::MissingPredecessor(*dependency))
188            })
189            .collect::<Result<Vec<_>, _>>()?;
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        let checkpoint = stage.reconcile_checkpoint(learned)?;
200        completed.insert(sequence, checkpoint);
201    }
202    Ok(read_complete_execution_workflow(layout, expected_workflow)?)
203}
204
205/// Failure of new original-workflow coordination; all originals remain retained.
206#[derive(Debug, Error)]
207pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
208    /// A validated predecessor was not available in the current run.
209    #[error("original workflow predecessor {0} did not complete")]
210    MissingPredecessor(u64),
211    /// Supplied identities do not cover exactly the occupied original stage paths.
212    #[error("retained stage identities do not match occupied original stages")]
213    RetainedStageSetMismatch,
214    /// An occupied successor froze a different learned predecessor commitment.
215    #[error("retained stage {0} has a different learned predecessor commitment")]
216    RetainedLearnedMismatch(u64),
217    /// Original allocation could not be published or was already occupied.
218    #[error(transparent)]
219    Workflow(#[from] ExecutionWorkflowPersistenceError),
220    /// A driver-supplied child violated the fixed original allocation.
221    #[error(transparent)]
222    Binding(#[from] ExecutionWorkflowError),
223    /// Complete child preparation failed without replacing original evidence.
224    #[error(transparent)]
225    Preparation(#[from] ExecutionStagePreparationError),
226    /// Driver-owned planning, qualification, provider or receipt step failed.
227    #[error("original workflow stage driver failed: {0}")]
228    Driver(#[source] E),
229    /// Canonical all-Applied checkpoint or final local replay failed.
230    #[error(transparent)]
231    Checkpoint(#[from] ExecutionStageCheckpointError),
232}
233
234#[cfg(test)]
235mod tests;