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. Pending, partially prepared or omitted occupied stages refuse;
135/// none is reissued. Only absent stages may be prepared and executed once, with
136/// fresh authority and external-effect admission remaining with the driver.
137/// This is no terminal receipt, same-ID restore or reference-release permit.
138/// # Errors
139/// Rejects incomplete or changed retained evidence before new driver callbacks;
140/// later stage failures preserve every original journal and effect obligation.
141pub async fn run_retained_execution_workflow<D: ExecutionWorkflowStageDriver>(
142    layout: &BackupLayoutGuard,
143    expected_workflow: &ArtifactChecksumRecord,
144    retained: &BTreeMap<u64, RetainedStageEvidence>,
145    driver: &mut D,
146) -> Result<ExecutionWorkflowSettlementView, ExecutionWorkflowRunError<D::Error>> {
147    let workflow = read_execution_workflow(layout, expected_workflow)?;
148    let occupied = read_execution_stage_occupancy(layout, expected_workflow)?;
149    if !retained.keys().copied().eq(occupied.iter().copied()) {
150        return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
151    }
152    let mut completed: BTreeMap<u64, ExecutionStagePredecessorRecord> = BTreeMap::new();
153    for (&sequence, evidence) in retained {
154        let stage =
155            ExecutionStageGuard::open(layout, expected_workflow, sequence, &evidence.binding)?;
156        let checkpoint = stage.replay_checkpoint(evidence.learned_evidence.clone())?;
157        completed.insert(sequence, checkpoint);
158    }
159    // Recheck the namespace before invoking any integration callback. Create-only
160    // stage preparation still refuses an occupied path after this preflight.
161    if read_execution_stage_occupancy(layout, expected_workflow)? != occupied {
162        return Err(ExecutionWorkflowRunError::RetainedStageSetMismatch);
163    }
164    for node in workflow.allocation().graph().ordered_nodes() {
165        let sequence = node.operation_sequence();
166        if completed.contains_key(&sequence) {
167            continue;
168        }
169        let predecessors = node
170            .depends_on()
171            .iter()
172            .map(|dependency| {
173                completed
174                    .get(dependency)
175                    .cloned()
176                    .ok_or(ExecutionWorkflowRunError::MissingPredecessor(*dependency))
177            })
178            .collect::<Result<Vec<_>, _>>()?;
179        let plan = driver
180            .bind_stage(&workflow, sequence, &predecessors)
181            .map_err(ExecutionWorkflowRunError::Driver)?;
182        let binding = ExecutionStageBindingRecord::new(&workflow, sequence, &plan, predecessors)?;
183        let stage = ExecutionStageGuard::prepare(layout, binding, plan)?;
184        let learned = driver
185            .execute_stage(&stage)
186            .await
187            .map_err(ExecutionWorkflowRunError::Driver)?;
188        let checkpoint = stage.reconcile_checkpoint(learned)?;
189        completed.insert(sequence, checkpoint);
190    }
191    Ok(read_complete_execution_workflow(layout, expected_workflow)?)
192}
193
194/// Failure of new original-workflow coordination; all originals remain retained.
195#[derive(Debug, Error)]
196pub enum ExecutionWorkflowRunError<E: std::error::Error + 'static> {
197    /// A validated predecessor was not available in the current run.
198    #[error("original workflow predecessor {0} did not complete")]
199    MissingPredecessor(u64),
200    /// Supplied identities do not cover exactly the occupied original stage paths.
201    #[error("retained stage identities do not match occupied original stages")]
202    RetainedStageSetMismatch,
203    /// Original allocation could not be published or was already occupied.
204    #[error(transparent)]
205    Workflow(#[from] ExecutionWorkflowPersistenceError),
206    /// A driver-supplied child violated the fixed original allocation.
207    #[error(transparent)]
208    Binding(#[from] ExecutionWorkflowError),
209    /// Complete child preparation failed without replacing original evidence.
210    #[error(transparent)]
211    Preparation(#[from] ExecutionStagePreparationError),
212    /// Driver-owned planning, qualification, provider or receipt step failed.
213    #[error("original workflow stage driver failed: {0}")]
214    Driver(#[source] E),
215    /// Canonical all-Applied checkpoint or final local replay failed.
216    #[error(transparent)]
217    Checkpoint(#[from] ExecutionStageCheckpointError),
218}
219
220#[cfg(test)]
221mod tests;