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    /// 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/// Coordinate one *new* fixed workflow in its declared dependency order.
111///
112/// Publish the original allocation once, prepare each child and all its original
113/// journals before invoking the driver, then reconcile its exact all-Applied
114/// checkpoint. The graph's topological order controls stage dependencies, never
115/// numeric sequence order. Success reads back every retained checkpoint through
116/// the canonical complete-workflow owner. No automatic outcome, retry, refund,
117/// terminal receipt, fence or reference release is inferred.
118///
119/// This is a deliberately one-shot entry point. A failure or cancellation retains
120/// the original workflow, prepared stages, pending spending, artifacts and all
121/// obligations. Calling it again refuses the occupied workflow *before* invoking
122/// the driver. Recovery must reopen and reconcile those exact originals through
123/// their separate owners. Retained continuation requires a separately qualified
124/// complete occupied-stage set; a new run cannot reconstruct authority from a prefix.
125/// # Errors
126/// Rejects occupied or changed originals, incomplete stages and driver failures,
127/// preserving all retained evidence without reissuing an effect.
128pub 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
166/// Continue an existing allocation after replaying every occupied settled stage.
167///
168/// The caller supplies the complete occupied stage set and authenticates each
169/// stage's learned evidence. Before any driver callback, the coordinator checks
170/// that set against the held layout and replays every exact all-Applied original
171/// checkpoint. Already bound successor edges must commit the qualified digest
172/// supplied for that exact edge; absent overrides use the stage checkpoint's
173/// learned digest. Pending, partially prepared or omitted occupied stages refuse;
174/// none is reissued. Only absent stages may be prepared and executed once, with
175/// fresh authority and external-effect admission remaining with the driver.
176/// This is no terminal receipt, same-ID restore or reference-release permit.
177/// # Errors
178/// Rejects incomplete or changed retained evidence before new driver callbacks;
179/// later stage failures preserve every original journal and effect obligation.
180pub 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    // Recheck the namespace before invoking any integration callback. Create-only
215    // stage preparation still refuses an occupied path after this preflight.
216    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/// Failure of new original-workflow coordination; all originals remain retained.
274#[derive(Debug, Error)]
275pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
276    /// A validated predecessor was not available in the current run.
277    #[error("original workflow predecessor {0} did not complete")]
278    MissingPredecessor(u64),
279    /// Supplied identities do not cover exactly the occupied original stage paths.
280    #[error("retained stage identities do not match occupied original stages")]
281    RetainedStageSetMismatch,
282    /// An occupied successor froze a different learned predecessor commitment,
283    /// or supplied edge evidence names a non-successor.
284    #[error("retained stage {0} has a different learned predecessor commitment")]
285    RetainedLearnedMismatch(u64),
286    /// Original allocation could not be published or was already occupied.
287    #[error(transparent)]
288    Workflow(#[from] ExecutionWorkflowPersistenceError),
289    /// A driver-supplied child violated the fixed original allocation.
290    #[error(transparent)]
291    Binding(#[from] ExecutionWorkflowError),
292    /// Complete child preparation failed without replacing original evidence.
293    #[error(transparent)]
294    Preparation(#[from] ExecutionStagePreparationError),
295    /// Driver-owned planning, qualification, provider or receipt step failed.
296    #[error("original workflow stage driver failed: {0}")]
297    Driver(#[source] E),
298    /// Canonical all-Applied checkpoint or final local replay failed.
299    #[error(transparent)]
300    Checkpoint(#[from] ExecutionStageCheckpointError),
301}
302
303#[cfg(test)]
304mod tests;