1use super::{
4 AttemptJournalError, AttemptJournalGuard, BackupLayoutGuard, ExecutionProgressPersistenceError,
5 ExecutionSettlementCheckpointError, ExecutionSettlementPersistenceError, JournalLock,
6 JournalLockError, OperationPlanPersistenceError, PersistenceError, read_execution_progress,
7 read_execution_settlement, read_json, read_operation_plan, reconcile_execution_settlement,
8 replay_execution_settlement,
9};
10use crate::model::{
11 artifacts::ArtifactChecksumRecord,
12 execution_workflow::{
13 ExecutionStageBindingRecord, ExecutionStagePredecessorRecord, ExecutionWorkflowError,
14 ExecutionWorkflowRecord, MAX_EXECUTION_STAGE_BYTES, MAX_EXECUTION_WORKFLOW_BYTES,
15 },
16 operation_plan::{OperationPlanError, OperationPlanRecord},
17};
18use crate::policy::execution_progress::ExecutionProgressView;
19use std::{
20 collections::{BTreeMap, BTreeSet, btree_map::Entry},
21 fs,
22 path::PathBuf,
23};
24use thiserror::Error;
25
26const WORKFLOW_FILE: &str = "execution-workflow.json";
27const BINDING_FILE: &str = "stage-binding.json";
28
29#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct ExecutionWorkflowSettlementView {
36 workflow: ArtifactChecksumRecord,
37 stages: Vec<ExecutionStageSettlementView>,
38}
39impl ExecutionWorkflowSettlementView {
40 #[must_use]
42 pub const fn workflow(&self) -> &ArtifactChecksumRecord {
43 &self.workflow
44 }
45 #[must_use]
47 pub fn stages(&self) -> &[ExecutionStageSettlementView] {
48 &self.stages
49 }
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct ExecutionStageSettlementView {
55 stage_sequence: u64,
56 binding: ArtifactChecksumRecord,
57 settlement: ArtifactChecksumRecord,
58}
59impl ExecutionStageSettlementView {
60 #[must_use]
62 pub const fn stage_sequence(&self) -> u64 {
63 self.stage_sequence
64 }
65 #[must_use]
67 pub const fn binding(&self) -> &ArtifactChecksumRecord {
68 &self.binding
69 }
70 #[must_use]
72 pub const fn settlement(&self) -> &ArtifactChecksumRecord {
73 &self.settlement
74 }
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78enum StagePreparationBarrier {
79 Plan,
80 Binding,
81 Journal(u64),
82}
83
84pub fn create_execution_workflow(
88 layout: &BackupLayoutGuard,
89 record: &ExecutionWorkflowRecord,
90) -> Result<(), ExecutionWorkflowPersistenceError> {
91 layout.check_root()?;
92 let bytes = super::json::encode_json(record, MAX_EXECUTION_WORKFLOW_BYTES)?;
93 let path = layout.root().join(WORKFLOW_FILE);
94 let _lock = JournalLock::acquire(&path)?;
95 super::json::create_encoded(&path, &bytes)?;
96 Ok(())
97}
98pub fn read_execution_workflow(
102 layout: &BackupLayoutGuard,
103 expected: &ArtifactChecksumRecord,
104) -> Result<ExecutionWorkflowRecord, ExecutionWorkflowPersistenceError> {
105 super::immutable_record::read(
106 layout,
107 WORKFLOW_FILE,
108 MAX_EXECUTION_WORKFLOW_BYTES,
109 |record: &ExecutionWorkflowRecord| &record.digest() == expected,
110 || ExecutionWorkflowPersistenceError::DigestMismatch,
111 )
112}
113
114pub(crate) fn read_execution_stage_occupancy(
122 layout: &BackupLayoutGuard,
123 expected: &ArtifactChecksumRecord,
124) -> Result<BTreeSet<u64>, ExecutionWorkflowPersistenceError> {
125 let workflow = read_execution_workflow(layout, expected)?;
126 let mut occupied = BTreeSet::new();
127 for stage in workflow.stages() {
128 let sequence = stage.operation_sequence();
129 match fs::symlink_metadata(stage_path(layout, sequence)) {
130 Ok(metadata) if metadata.is_dir() => {
131 occupied.insert(sequence);
132 }
133 Ok(_) => return Err(ExecutionWorkflowPersistenceError::UnsafeStage),
134 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
135 Err(error) => return Err(PersistenceError::from(error).into()),
136 }
137 }
138 read_execution_workflow(layout, expected)?;
139 Ok(occupied)
140}
141
142pub fn read_complete_execution_workflow(
155 layout: &BackupLayoutGuard,
156 expected: &ArtifactChecksumRecord,
157) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
158 read_complete_with(layout, expected, || {
159 read_execution_workflow(layout, expected)
160 })
161}
162
163pub fn read_complete_execution_workflow_locked(
173 layout: &BackupLayoutGuard,
174 journal: &JournalLock,
175 expected: &ArtifactChecksumRecord,
176) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
177 if journal.journal() != layout.root().join(WORKFLOW_FILE) {
178 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
179 }
180 read_complete_with(layout, expected, || {
181 layout.check_root()?;
182 let workflow: ExecutionWorkflowRecord =
183 read_json(journal.journal(), MAX_EXECUTION_WORKFLOW_BYTES)?;
184 super::json::check_json_size(&workflow, MAX_EXECUTION_WORKFLOW_BYTES)?;
185 if &workflow.digest() != expected {
186 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
187 }
188 Ok(workflow)
189 })
190}
191
192fn read_complete_with(
193 layout: &BackupLayoutGuard,
194 expected: &ArtifactChecksumRecord,
195 read_original: impl Fn() -> Result<ExecutionWorkflowRecord, ExecutionWorkflowPersistenceError>,
196) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
197 let workflow = read_original()?;
198 let mut stages = Vec::with_capacity(workflow.stages().len());
199 let mut bindings = Vec::with_capacity(workflow.stages().len());
200 let mut by_sequence = BTreeMap::new();
201 for allocated in workflow.stages() {
202 let sequence = allocated.operation_sequence();
203 let stage = acquire_stage(layout, sequence)?;
204 let binding: ExecutionStageBindingRecord =
205 read_json(&stage.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)
206 .map_err(ExecutionWorkflowPersistenceError::from)?;
207 let (binding, plan) = read_binding(&stage, &workflow, sequence, &binding.digest())?;
208 let settlement = replay_execution_settlement(&stage, &plan.digest())?;
209 read_binding(&stage, &workflow, sequence, &binding.digest())?;
210 by_sequence.insert(sequence, stages.len());
211 stages.push(ExecutionStageSettlementView {
212 stage_sequence: sequence,
213 binding: binding.digest(),
214 settlement: settlement.digest(),
215 });
216 bindings.push(binding);
217 }
218 for binding in &bindings {
219 for predecessor in binding.predecessors() {
220 let Some(&index) = by_sequence.get(&predecessor.stage_sequence()) else {
221 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
222 };
223 let actual = &stages[index];
224 if predecessor.binding() != actual.binding()
225 || predecessor.settlement() != actual.settlement()
226 {
227 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
228 }
229 }
230 }
231 read_original()?;
232 Ok(ExecutionWorkflowSettlementView {
233 workflow: expected.clone(),
234 stages,
235 })
236}
237
238#[derive(Debug)]
246pub struct ExecutionStageGuard<'a> {
247 workflow_layout: &'a BackupLayoutGuard,
248 stage_layout: BackupLayoutGuard,
249 binding: ExecutionStageBindingRecord,
250 plan: OperationPlanRecord,
251}
252impl<'a> ExecutionStageGuard<'a> {
253 pub fn reconcile_checkpoint(
263 &self,
264 learned_evidence: ArtifactChecksumRecord,
265 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
266 self.reconcile_checkpoint_with(learned_evidence, || {})
267 }
268 fn reconcile_checkpoint_with(
269 &self,
270 learned_evidence: ArtifactChecksumRecord,
271 after_reconciliation: impl FnOnce(),
272 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
273 let settlement = reconcile_execution_settlement(self.layout()?, &self.plan.digest())?;
274 after_reconciliation();
275 self.layout()?;
276 Ok(ExecutionStagePredecessorRecord::new(
277 self.binding.stage_sequence(),
278 self.binding.digest(),
279 settlement.digest(),
280 learned_evidence,
281 ))
282 }
283 pub fn replay_checkpoint(
292 &self,
293 learned_evidence: ArtifactChecksumRecord,
294 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
295 let settlement = replay_execution_settlement(self.layout()?, &self.plan.digest())?;
296 self.layout()?;
297 Ok(ExecutionStagePredecessorRecord::new(
298 self.binding.stage_sequence(),
299 self.binding.digest(),
300 settlement.digest(),
301 learned_evidence,
302 ))
303 }
304 pub fn resume(
317 workflow_layout: &'a BackupLayoutGuard,
318 expected_workflow: &ArtifactChecksumRecord,
319 sequence: u64,
320 expected_binding: &ArtifactChecksumRecord,
321 ) -> Result<(Self, ExecutionProgressView), ExecutionStageResumeError> {
322 let stage = Self::open(
323 workflow_layout,
324 expected_workflow,
325 sequence,
326 expected_binding,
327 )?;
328 let progress = read_execution_progress(&stage.stage_layout, &stage.plan.digest())?;
329 stage.layout()?;
330 Ok((stage, progress))
331 }
332 pub fn prepare(
347 workflow_layout: &'a BackupLayoutGuard,
348 binding: ExecutionStageBindingRecord,
349 plan: OperationPlanRecord,
350 ) -> Result<Self, ExecutionStagePreparationError> {
351 Self::prepare_with(workflow_layout, binding, plan, |_| {})
352 }
353 fn prepare_with(
354 workflow_layout: &'a BackupLayoutGuard,
355 binding: ExecutionStageBindingRecord,
356 plan: OperationPlanRecord,
357 mut barrier: impl FnMut(StagePreparationBarrier),
358 ) -> Result<Self, ExecutionStagePreparationError> {
359 let authorities = plan.attempt_authorities()?;
360 let stage = Self::create_with(workflow_layout, binding, plan, &mut barrier)?;
361 let layout = stage.layout()?;
362 for authority in authorities {
363 let sequence = authority.binding().operation_sequence();
364 drop(AttemptJournalGuard::create(layout, authority)?);
365 barrier(StagePreparationBarrier::Journal(sequence));
366 }
367 stage.layout()?;
368 Ok(stage)
369 }
370 pub fn create(
379 workflow_layout: &'a BackupLayoutGuard,
380 binding: ExecutionStageBindingRecord,
381 plan: OperationPlanRecord,
382 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
383 Self::create_with(workflow_layout, binding, plan, |_| {})
384 }
385 fn create_with(
386 workflow_layout: &'a BackupLayoutGuard,
387 binding: ExecutionStageBindingRecord,
388 plan: OperationPlanRecord,
389 mut barrier: impl FnMut(StagePreparationBarrier),
390 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
391 let workflow = read_execution_workflow(workflow_layout, binding.workflow())?;
392 binding.validate(&workflow, &plan)?;
393 let binding_bytes = super::json::encode_json(&binding, MAX_EXECUTION_STAGE_BYTES)?;
394 let plan_bytes = super::json::encode_json(
396 &plan,
397 crate::model::operation_plan::MAX_OPERATION_PLAN_BYTES,
398 )?;
399 validate_predecessors(workflow_layout, &workflow, &binding)?;
400 let path = stage_path(workflow_layout, binding.stage_sequence());
401 ic_host_fs::durable::create_private_directory(&path)
402 .map_err(|error| PersistenceError::Io(crate::ops::directory_creation_io(error)))?;
403 let stage_layout = acquire_stage(workflow_layout, binding.stage_sequence())?;
404 super::operation_plan::create_encoded(&stage_layout, &plan_bytes)?;
405 barrier(StagePreparationBarrier::Plan);
406 super::json::create_encoded(&stage_layout.root().join(BINDING_FILE), &binding_bytes)?;
407 barrier(StagePreparationBarrier::Binding);
408 workflow_layout.check_root()?;
409 Ok(Self {
410 workflow_layout,
411 stage_layout,
412 binding,
413 plan,
414 })
415 }
416 pub fn open(
425 workflow_layout: &'a BackupLayoutGuard,
426 expected_workflow: &ArtifactChecksumRecord,
427 sequence: u64,
428 expected_binding: &ArtifactChecksumRecord,
429 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
430 let workflow = read_execution_workflow(workflow_layout, expected_workflow)?;
431 workflow
432 .stage(sequence)
433 .map_err(|_| ExecutionWorkflowError::UnknownStage(sequence))?;
434 let stage_layout = acquire_stage(workflow_layout, sequence)?;
435 let (binding, plan) = read_binding(&stage_layout, &workflow, sequence, expected_binding)?;
436 validate_predecessors(workflow_layout, &workflow, &binding)?;
437 workflow_layout.check_root()?;
438 Ok(Self {
439 workflow_layout,
440 stage_layout,
441 binding,
442 plan,
443 })
444 }
445 #[must_use]
447 pub const fn plan(&self) -> &OperationPlanRecord {
448 &self.plan
449 }
450 #[must_use]
452 pub const fn binding(&self) -> &ExecutionStageBindingRecord {
453 &self.binding
454 }
455 pub fn layout(&self) -> Result<&BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
462 let workflow = read_execution_workflow(self.workflow_layout, self.binding.workflow())?;
463 read_binding(
464 &self.stage_layout,
465 &workflow,
466 self.binding.stage_sequence(),
467 &self.binding.digest(),
468 )?;
469 validate_predecessors(self.workflow_layout, &workflow, &self.binding)?;
470 Ok(&self.stage_layout)
471 }
472}
473
474#[derive(Debug, Error)]
476pub enum ExecutionStageCheckpointError {
477 #[error(transparent)]
479 Stage(#[from] ExecutionWorkflowPersistenceError),
480 #[error(transparent)]
482 Settlement(#[from] ExecutionSettlementCheckpointError),
483}
484
485#[derive(Debug, Error)]
487pub enum ExecutionStagePreparationError {
488 #[error(transparent)]
490 Stage(#[from] ExecutionWorkflowPersistenceError),
491 #[error(transparent)]
493 Authority(#[from] OperationPlanError),
494 #[error(transparent)]
496 Journal(#[from] AttemptJournalError),
497}
498
499#[derive(Debug, Error)]
501pub enum ExecutionStageResumeError {
502 #[error(transparent)]
504 Stage(#[from] ExecutionWorkflowPersistenceError),
505 #[error(transparent)]
507 Progress(#[from] ExecutionProgressPersistenceError),
508}
509fn read_binding(
510 layout: &BackupLayoutGuard,
511 workflow: &ExecutionWorkflowRecord,
512 sequence: u64,
513 expected: &ArtifactChecksumRecord,
514) -> Result<(ExecutionStageBindingRecord, OperationPlanRecord), ExecutionWorkflowPersistenceError> {
515 layout.check_root()?;
516 let binding: ExecutionStageBindingRecord =
517 read_json(&layout.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)?;
518 super::json::check_json_size(&binding, MAX_EXECUTION_STAGE_BYTES)?;
519 if binding.stage_sequence() != sequence || &binding.digest() != expected {
520 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
521 }
522 let plan = read_operation_plan(layout, binding.plan())?;
523 binding.validate(workflow, &plan)?;
524 Ok((binding, plan))
525}
526fn validate_predecessors(
527 layout: &BackupLayoutGuard,
528 workflow: &ExecutionWorkflowRecord,
529 binding: &ExecutionStageBindingRecord,
530) -> Result<(), ExecutionWorkflowPersistenceError> {
531 let mut expected: BTreeMap<u64, ExecutionStagePredecessorRecord> = binding
535 .predecessors()
536 .iter()
537 .map(|row| (row.stage_sequence(), row.clone()))
538 .collect();
539 let mut pending: Vec<_> = expected.keys().copied().collect();
540 while let Some(sequence) = pending.pop() {
541 let row = &expected[&sequence];
542 let original = {
543 let predecessor = acquire_stage(layout, sequence)?;
546 let (original, _) = read_binding(&predecessor, workflow, sequence, row.binding())?;
547 read_execution_settlement(&predecessor, original.plan(), row.settlement())?;
548 original
549 };
550 for ancestor in original.predecessors() {
551 match expected.entry(ancestor.stage_sequence()) {
552 Entry::Vacant(entry) => {
553 pending.push(ancestor.stage_sequence());
554 entry.insert(ancestor.clone());
555 }
556 Entry::Occupied(entry) => {
557 if entry.get().binding() != ancestor.binding()
558 || entry.get().settlement() != ancestor.settlement()
559 {
560 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
561 }
562 }
563 }
564 }
565 }
566 Ok(())
567}
568fn stage_path(layout: &BackupLayoutGuard, sequence: u64) -> PathBuf {
569 layout.root().join(format!("execution-stage-{sequence}"))
570}
571fn acquire_stage(
572 layout: &BackupLayoutGuard,
573 sequence: u64,
574) -> Result<BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
575 layout.check_root()?;
576 let path = stage_path(layout, sequence);
577 let metadata = fs::symlink_metadata(&path).map_err(PersistenceError::from)?;
578 if !metadata.is_dir() {
579 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
580 }
581 let stage = BackupLayoutGuard::acquire(&path)?;
582 if stage.root() != path {
585 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
586 }
587 layout.check_root()?;
588 Ok(stage)
589}
590#[derive(Debug, Error)]
592pub enum ExecutionWorkflowPersistenceError {
593 #[error("execution workflow/stage digest mismatch")]
595 DigestMismatch,
596 #[error("unsafe execution stage directory")]
598 UnsafeStage,
599 #[error(transparent)]
601 Model(#[from] ExecutionWorkflowError),
602 #[error(transparent)]
604 Plan(#[from] OperationPlanPersistenceError),
605 #[error(transparent)]
607 Settlement(#[from] ExecutionSettlementPersistenceError),
608 #[error(transparent)]
610 Lock(#[from] JournalLockError),
611 #[error(transparent)]
613 Persistence(#[from] PersistenceError),
614}
615
616#[cfg(all(test, unix))]
617mod tests;