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 fn create_with(
372 workflow_layout: &'a BackupLayoutGuard,
373 binding: ExecutionStageBindingRecord,
374 plan: OperationPlanRecord,
375 mut barrier: impl FnMut(StagePreparationBarrier),
376 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
377 let workflow = read_execution_workflow(workflow_layout, binding.workflow())?;
378 binding.validate(&workflow, &plan)?;
379 let binding_bytes = super::json::encode_json(&binding, MAX_EXECUTION_STAGE_BYTES)?;
380 let plan_bytes = super::json::encode_json(
382 &plan,
383 crate::model::operation_plan::MAX_OPERATION_PLAN_BYTES,
384 )?;
385 validate_predecessors(workflow_layout, &workflow, &binding)?;
386 let path = stage_path(workflow_layout, binding.stage_sequence());
387 ic_host_fs::durable::create_private_directory(&path)
388 .map_err(|error| PersistenceError::Io(crate::ops::directory_creation_io(error)))?;
389 let stage_layout = acquire_stage(workflow_layout, binding.stage_sequence())?;
390 super::operation_plan::create_encoded(&stage_layout, &plan_bytes)?;
391 barrier(StagePreparationBarrier::Plan);
392 super::json::create_encoded(&stage_layout.root().join(BINDING_FILE), &binding_bytes)?;
393 barrier(StagePreparationBarrier::Binding);
394 workflow_layout.check_root()?;
395 Ok(Self {
396 workflow_layout,
397 stage_layout,
398 binding,
399 plan,
400 })
401 }
402 pub fn open(
411 workflow_layout: &'a BackupLayoutGuard,
412 expected_workflow: &ArtifactChecksumRecord,
413 sequence: u64,
414 expected_binding: &ArtifactChecksumRecord,
415 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
416 let workflow = read_execution_workflow(workflow_layout, expected_workflow)?;
417 workflow
418 .stage(sequence)
419 .map_err(|_| ExecutionWorkflowError::UnknownStage(sequence))?;
420 let stage_layout = acquire_stage(workflow_layout, sequence)?;
421 let (binding, plan) = read_binding(&stage_layout, &workflow, sequence, expected_binding)?;
422 validate_predecessors(workflow_layout, &workflow, &binding)?;
423 workflow_layout.check_root()?;
424 Ok(Self {
425 workflow_layout,
426 stage_layout,
427 binding,
428 plan,
429 })
430 }
431 #[must_use]
433 pub const fn plan(&self) -> &OperationPlanRecord {
434 &self.plan
435 }
436 #[must_use]
438 pub const fn binding(&self) -> &ExecutionStageBindingRecord {
439 &self.binding
440 }
441 pub fn layout(&self) -> Result<&BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
448 let workflow = read_execution_workflow(self.workflow_layout, self.binding.workflow())?;
449 read_binding(
450 &self.stage_layout,
451 &workflow,
452 self.binding.stage_sequence(),
453 &self.binding.digest(),
454 )?;
455 validate_predecessors(self.workflow_layout, &workflow, &self.binding)?;
456 Ok(&self.stage_layout)
457 }
458}
459
460#[derive(Debug, Error)]
462pub enum ExecutionStageCheckpointError {
463 #[error(transparent)]
465 Stage(#[from] ExecutionWorkflowPersistenceError),
466 #[error(transparent)]
468 Settlement(#[from] ExecutionSettlementCheckpointError),
469}
470
471#[derive(Debug, Error)]
473pub enum ExecutionStagePreparationError {
474 #[error(transparent)]
476 Stage(#[from] ExecutionWorkflowPersistenceError),
477 #[error(transparent)]
479 Authority(#[from] OperationPlanError),
480 #[error(transparent)]
482 Journal(#[from] AttemptJournalError),
483}
484
485#[derive(Debug, Error)]
487pub enum ExecutionStageResumeError {
488 #[error(transparent)]
490 Stage(#[from] ExecutionWorkflowPersistenceError),
491 #[error(transparent)]
493 Progress(#[from] ExecutionProgressPersistenceError),
494}
495fn read_binding(
496 layout: &BackupLayoutGuard,
497 workflow: &ExecutionWorkflowRecord,
498 sequence: u64,
499 expected: &ArtifactChecksumRecord,
500) -> Result<(ExecutionStageBindingRecord, OperationPlanRecord), ExecutionWorkflowPersistenceError> {
501 layout.check_root()?;
502 let binding: ExecutionStageBindingRecord =
503 read_json(&layout.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)?;
504 super::json::check_json_size(&binding, MAX_EXECUTION_STAGE_BYTES)?;
505 if binding.stage_sequence() != sequence || &binding.digest() != expected {
506 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
507 }
508 let plan = read_operation_plan(layout, binding.plan())?;
509 binding.validate(workflow, &plan)?;
510 Ok((binding, plan))
511}
512fn validate_predecessors(
513 layout: &BackupLayoutGuard,
514 workflow: &ExecutionWorkflowRecord,
515 binding: &ExecutionStageBindingRecord,
516) -> Result<(), ExecutionWorkflowPersistenceError> {
517 let mut expected: BTreeMap<u64, ExecutionStagePredecessorRecord> = binding
521 .predecessors()
522 .iter()
523 .map(|row| (row.stage_sequence(), row.clone()))
524 .collect();
525 let mut pending: Vec<_> = expected.keys().copied().collect();
526 while let Some(sequence) = pending.pop() {
527 let row = &expected[&sequence];
528 let original = {
529 let predecessor = acquire_stage(layout, sequence)?;
532 let (original, _) = read_binding(&predecessor, workflow, sequence, row.binding())?;
533 read_execution_settlement(&predecessor, original.plan(), row.settlement())?;
534 original
535 };
536 for ancestor in original.predecessors() {
537 match expected.entry(ancestor.stage_sequence()) {
538 Entry::Vacant(entry) => {
539 pending.push(ancestor.stage_sequence());
540 entry.insert(ancestor.clone());
541 }
542 Entry::Occupied(entry) => {
543 if entry.get().binding() != ancestor.binding()
544 || entry.get().settlement() != ancestor.settlement()
545 {
546 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
547 }
548 }
549 }
550 }
551 }
552 Ok(())
553}
554fn stage_path(layout: &BackupLayoutGuard, sequence: u64) -> PathBuf {
555 layout.root().join(format!("execution-stage-{sequence}"))
556}
557fn acquire_stage(
558 layout: &BackupLayoutGuard,
559 sequence: u64,
560) -> Result<BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
561 layout.check_root()?;
562 let path = stage_path(layout, sequence);
563 let metadata = fs::symlink_metadata(&path).map_err(PersistenceError::from)?;
564 if !metadata.is_dir() {
565 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
566 }
567 let stage = BackupLayoutGuard::acquire(&path)?;
568 if stage.root() != path {
571 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
572 }
573 layout.check_root()?;
574 Ok(stage)
575}
576#[derive(Debug, Error)]
578pub enum ExecutionWorkflowPersistenceError {
579 #[error("execution workflow/stage digest mismatch")]
581 DigestMismatch,
582 #[error("unsafe execution stage directory")]
584 UnsafeStage,
585 #[error(transparent)]
587 Model(#[from] ExecutionWorkflowError),
588 #[error(transparent)]
590 Plan(#[from] OperationPlanPersistenceError),
591 #[error(transparent)]
593 Settlement(#[from] ExecutionSettlementPersistenceError),
594 #[error(transparent)]
596 Lock(#[from] JournalLockError),
597 #[error(transparent)]
599 Persistence(#[from] PersistenceError),
600}
601
602#[cfg(all(test, unix))]
603mod tests;