ic_backup/ops/persistence/execution_workflow/
mod.rs1mod ancestry;
4mod stage;
5use ancestry::{
6 acquire_stage, read_admitted_binding, read_binding, stage_path, validate_predecessors,
7};
8pub use stage::ExecutionStageGuard;
9#[cfg(test)]
10use stage::StagePreparationBarrier;
11
12use super::{
13 AttemptJournalError, AttemptJournalGuard, BackupLayoutGuard, ExecutionProgressPersistenceError,
14 ExecutionSettlementCheckpointError, ExecutionSettlementPersistenceError, JournalLock,
15 JournalLockError, OperationPlanPersistenceError, PersistenceError, read_execution_progress,
16 read_execution_settlement, read_json, read_operation_plan, reconcile_execution_settlement,
17 replay_execution_settlement,
18};
19use crate::model::{
20 artifacts::ArtifactChecksumRecord,
21 execution_workflow::{
22 ExecutionStageBindingRecord, ExecutionStagePredecessorRecord, ExecutionWorkflowError,
23 ExecutionWorkflowRecord, MAX_EXECUTION_STAGE_BYTES, MAX_EXECUTION_WORKFLOW_BYTES,
24 },
25 operation_plan::{OperationPlanError, OperationPlanRecord},
26};
27use crate::policy::execution_progress::ExecutionProgressView;
28use std::{
29 collections::{BTreeMap, BTreeSet, btree_map::Entry},
30 fs,
31 path::PathBuf,
32};
33use thiserror::Error;
34
35const WORKFLOW_FILE: &str = "execution-workflow.json";
36const BINDING_FILE: &str = "stage-binding.json";
37
38#[derive(Clone, Debug, Eq, PartialEq)]
44pub struct ExecutionWorkflowSettlementView {
45 workflow: ArtifactChecksumRecord,
46 stages: Vec<ExecutionStageSettlementView>,
47}
48impl ExecutionWorkflowSettlementView {
49 #[must_use]
51 pub const fn workflow(&self) -> &ArtifactChecksumRecord {
52 &self.workflow
53 }
54 #[must_use]
56 pub fn stages(&self) -> &[ExecutionStageSettlementView] {
57 &self.stages
58 }
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct ExecutionStageSettlementView {
64 stage_sequence: u64,
65 binding: ArtifactChecksumRecord,
66 settlement: ArtifactChecksumRecord,
67}
68impl ExecutionStageSettlementView {
69 #[must_use]
71 pub const fn stage_sequence(&self) -> u64 {
72 self.stage_sequence
73 }
74 #[must_use]
76 pub const fn binding(&self) -> &ArtifactChecksumRecord {
77 &self.binding
78 }
79 #[must_use]
81 pub const fn settlement(&self) -> &ArtifactChecksumRecord {
82 &self.settlement
83 }
84}
85
86pub fn create_execution_workflow(
90 layout: &BackupLayoutGuard,
91 record: &ExecutionWorkflowRecord,
92) -> Result<(), ExecutionWorkflowPersistenceError> {
93 layout.check_root()?;
94 let bytes = super::json::encode_json(record, MAX_EXECUTION_WORKFLOW_BYTES)?;
95 super::immutable_record::create_encoded_record(layout, WORKFLOW_FILE, &bytes)
96}
97pub fn read_execution_workflow(
101 layout: &BackupLayoutGuard,
102 expected: &ArtifactChecksumRecord,
103) -> Result<ExecutionWorkflowRecord, ExecutionWorkflowPersistenceError> {
104 super::immutable_record::read(
105 layout,
106 WORKFLOW_FILE,
107 MAX_EXECUTION_WORKFLOW_BYTES,
108 |record: &ExecutionWorkflowRecord| &record.digest() == expected,
109 || ExecutionWorkflowPersistenceError::DigestMismatch,
110 )
111}
112
113pub(crate) fn read_execution_stage_occupancy(
121 layout: &BackupLayoutGuard,
122 expected: &ArtifactChecksumRecord,
123) -> Result<BTreeSet<u64>, ExecutionWorkflowPersistenceError> {
124 let workflow = read_execution_workflow(layout, expected)?;
125 let mut occupied = BTreeSet::new();
126 for stage in workflow.stages() {
127 let sequence = stage.operation_sequence();
128 match fs::symlink_metadata(stage_path(layout, sequence)) {
129 Ok(metadata) if metadata.is_dir() => {
130 occupied.insert(sequence);
131 }
132 Ok(_) => return Err(ExecutionWorkflowPersistenceError::UnsafeStage),
133 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
134 Err(error) => return Err(PersistenceError::from(error).into()),
135 }
136 }
137 read_execution_workflow(layout, expected)?;
138 Ok(occupied)
139}
140
141pub fn read_complete_execution_workflow(
154 layout: &BackupLayoutGuard,
155 expected: &ArtifactChecksumRecord,
156) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
157 read_complete_with(layout, expected, || {
158 read_execution_workflow(layout, expected)
159 })
160}
161
162pub fn read_complete_execution_workflow_locked(
172 layout: &BackupLayoutGuard,
173 journal: &JournalLock,
174 expected: &ArtifactChecksumRecord,
175) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
176 if journal.journal() != layout.root().join(WORKFLOW_FILE) {
177 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
178 }
179 read_complete_with(layout, expected, || {
180 layout.check_root()?;
181 let workflow: ExecutionWorkflowRecord =
182 read_json(journal.journal(), MAX_EXECUTION_WORKFLOW_BYTES)?;
183 super::json::check_json_size(&workflow, MAX_EXECUTION_WORKFLOW_BYTES)?;
184 if &workflow.digest() != expected {
185 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
186 }
187 Ok(workflow)
188 })
189}
190
191fn read_complete_with(
192 layout: &BackupLayoutGuard,
193 expected: &ArtifactChecksumRecord,
194 read_original: impl Fn() -> Result<ExecutionWorkflowRecord, ExecutionWorkflowPersistenceError>,
195) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
196 let workflow = read_original()?;
197 let mut stages = Vec::with_capacity(workflow.stages().len());
198 for allocated in workflow.stages() {
199 let sequence = allocated.operation_sequence();
200 let stage = acquire_stage(layout, sequence)?;
201 let binding: ExecutionStageBindingRecord =
202 read_json(&stage.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)
203 .map_err(ExecutionWorkflowPersistenceError::from)?;
204 let (binding, plan) = read_binding(&stage, &workflow, sequence, &binding.digest())?;
205 let settlement = replay_execution_settlement(&stage, &plan.digest())?;
206 read_binding(&stage, &workflow, sequence, &binding.digest())?;
207 let view = ExecutionStageSettlementView {
208 stage_sequence: sequence,
209 binding: binding.digest(),
210 settlement: settlement.digest(),
211 };
212 stages.push((binding, view));
213 }
214 for (binding, _) in &stages {
215 for predecessor in binding.predecessors() {
216 let index = stages
218 .binary_search_by_key(&predecessor.stage_sequence(), |(_, view)| {
219 view.stage_sequence()
220 })
221 .map_err(|_| ExecutionWorkflowPersistenceError::DigestMismatch)?;
222 let actual = &stages[index].1;
223 if predecessor.binding() != actual.binding()
224 || predecessor.settlement() != actual.settlement()
225 {
226 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
227 }
228 }
229 }
230 read_original()?;
231 Ok(ExecutionWorkflowSettlementView {
232 workflow: expected.clone(),
233 stages: stages.into_iter().map(|(_, view)| view).collect(),
234 })
235}
236
237#[derive(Debug, Error)]
239pub enum ExecutionStageCheckpointError {
240 #[error(transparent)]
242 Stage(#[from] ExecutionWorkflowPersistenceError),
243 #[error(transparent)]
245 Settlement(#[from] ExecutionSettlementCheckpointError),
246}
247
248#[derive(Debug, Error)]
250pub enum ExecutionStagePreparationError {
251 #[error(transparent)]
253 Stage(#[from] ExecutionWorkflowPersistenceError),
254 #[error(transparent)]
256 Authority(#[from] OperationPlanError),
257 #[error(transparent)]
259 Journal(#[from] AttemptJournalError),
260}
261
262#[derive(Debug, Error)]
264pub enum ExecutionStageResumeError {
265 #[error(transparent)]
267 Stage(#[from] ExecutionWorkflowPersistenceError),
268 #[error(transparent)]
270 Progress(#[from] ExecutionProgressPersistenceError),
271}
272#[derive(Debug, Error)]
274pub enum ExecutionWorkflowPersistenceError {
275 #[error("execution workflow/stage digest mismatch")]
277 DigestMismatch,
278 #[error("unsafe execution stage directory")]
280 UnsafeStage,
281 #[error(transparent)]
283 Model(#[from] ExecutionWorkflowError),
284 #[error(transparent)]
286 Plan(#[from] OperationPlanPersistenceError),
287 #[error(transparent)]
289 Settlement(#[from] ExecutionSettlementPersistenceError),
290 #[error(transparent)]
292 Lock(#[from] JournalLockError),
293 #[error(transparent)]
295 Persistence(#[from] PersistenceError),
296}
297
298#[cfg(all(test, unix))]
299mod tests;