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 super::immutable_record::create_encoded_record(layout, WORKFLOW_FILE, &bytes)
94}
95pub fn read_execution_workflow(
99 layout: &BackupLayoutGuard,
100 expected: &ArtifactChecksumRecord,
101) -> Result<ExecutionWorkflowRecord, ExecutionWorkflowPersistenceError> {
102 super::immutable_record::read(
103 layout,
104 WORKFLOW_FILE,
105 MAX_EXECUTION_WORKFLOW_BYTES,
106 |record: &ExecutionWorkflowRecord| &record.digest() == expected,
107 || ExecutionWorkflowPersistenceError::DigestMismatch,
108 )
109}
110
111pub(crate) fn read_execution_stage_occupancy(
119 layout: &BackupLayoutGuard,
120 expected: &ArtifactChecksumRecord,
121) -> Result<BTreeSet<u64>, ExecutionWorkflowPersistenceError> {
122 let workflow = read_execution_workflow(layout, expected)?;
123 let mut occupied = BTreeSet::new();
124 for stage in workflow.stages() {
125 let sequence = stage.operation_sequence();
126 match fs::symlink_metadata(stage_path(layout, sequence)) {
127 Ok(metadata) if metadata.is_dir() => {
128 occupied.insert(sequence);
129 }
130 Ok(_) => return Err(ExecutionWorkflowPersistenceError::UnsafeStage),
131 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
132 Err(error) => return Err(PersistenceError::from(error).into()),
133 }
134 }
135 read_execution_workflow(layout, expected)?;
136 Ok(occupied)
137}
138
139pub fn read_complete_execution_workflow(
152 layout: &BackupLayoutGuard,
153 expected: &ArtifactChecksumRecord,
154) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
155 read_complete_with(layout, expected, || {
156 read_execution_workflow(layout, expected)
157 })
158}
159
160pub fn read_complete_execution_workflow_locked(
170 layout: &BackupLayoutGuard,
171 journal: &JournalLock,
172 expected: &ArtifactChecksumRecord,
173) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
174 if journal.journal() != layout.root().join(WORKFLOW_FILE) {
175 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
176 }
177 read_complete_with(layout, expected, || {
178 layout.check_root()?;
179 let workflow: ExecutionWorkflowRecord =
180 read_json(journal.journal(), MAX_EXECUTION_WORKFLOW_BYTES)?;
181 super::json::check_json_size(&workflow, MAX_EXECUTION_WORKFLOW_BYTES)?;
182 if &workflow.digest() != expected {
183 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
184 }
185 Ok(workflow)
186 })
187}
188
189fn read_complete_with(
190 layout: &BackupLayoutGuard,
191 expected: &ArtifactChecksumRecord,
192 read_original: impl Fn() -> Result<ExecutionWorkflowRecord, ExecutionWorkflowPersistenceError>,
193) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
194 let workflow = read_original()?;
195 let mut stages = Vec::with_capacity(workflow.stages().len());
196 let mut bindings = Vec::with_capacity(workflow.stages().len());
197 let mut by_sequence = BTreeMap::new();
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 by_sequence.insert(sequence, stages.len());
208 stages.push(ExecutionStageSettlementView {
209 stage_sequence: sequence,
210 binding: binding.digest(),
211 settlement: settlement.digest(),
212 });
213 bindings.push(binding);
214 }
215 for binding in &bindings {
216 for predecessor in binding.predecessors() {
217 let Some(&index) = by_sequence.get(&predecessor.stage_sequence()) else {
218 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
219 };
220 let actual = &stages[index];
221 if predecessor.binding() != actual.binding()
222 || predecessor.settlement() != actual.settlement()
223 {
224 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
225 }
226 }
227 }
228 read_original()?;
229 Ok(ExecutionWorkflowSettlementView {
230 workflow: expected.clone(),
231 stages,
232 })
233}
234
235#[derive(Debug)]
243pub struct ExecutionStageGuard<'a> {
244 workflow_layout: &'a BackupLayoutGuard,
245 stage_layout: BackupLayoutGuard,
246 binding: ExecutionStageBindingRecord,
247 plan: OperationPlanRecord,
248}
249impl<'a> ExecutionStageGuard<'a> {
250 pub fn reconcile_checkpoint(
260 &self,
261 learned_evidence: ArtifactChecksumRecord,
262 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
263 self.reconcile_checkpoint_with(learned_evidence, || {})
264 }
265 fn reconcile_checkpoint_with(
266 &self,
267 learned_evidence: ArtifactChecksumRecord,
268 after_reconciliation: impl FnOnce(),
269 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
270 let settlement = reconcile_execution_settlement(self.layout()?, &self.plan.digest())?;
271 after_reconciliation();
272 self.layout()?;
273 Ok(ExecutionStagePredecessorRecord::new(
274 self.binding.stage_sequence(),
275 self.binding.digest(),
276 settlement.digest(),
277 learned_evidence,
278 ))
279 }
280 pub fn replay_checkpoint(
289 &self,
290 learned_evidence: ArtifactChecksumRecord,
291 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
292 let settlement = replay_execution_settlement(self.layout()?, &self.plan.digest())?;
293 self.layout()?;
294 Ok(ExecutionStagePredecessorRecord::new(
295 self.binding.stage_sequence(),
296 self.binding.digest(),
297 settlement.digest(),
298 learned_evidence,
299 ))
300 }
301 pub fn resume(
314 workflow_layout: &'a BackupLayoutGuard,
315 expected_workflow: &ArtifactChecksumRecord,
316 sequence: u64,
317 expected_binding: &ArtifactChecksumRecord,
318 ) -> Result<(Self, ExecutionProgressView), ExecutionStageResumeError> {
319 let stage = Self::open(
320 workflow_layout,
321 expected_workflow,
322 sequence,
323 expected_binding,
324 )?;
325 let progress = read_execution_progress(&stage.stage_layout, &stage.plan.digest())?;
326 stage.layout()?;
327 Ok((stage, progress))
328 }
329 pub fn prepare(
344 workflow_layout: &'a BackupLayoutGuard,
345 binding: ExecutionStageBindingRecord,
346 plan: OperationPlanRecord,
347 ) -> Result<Self, ExecutionStagePreparationError> {
348 Self::prepare_with(workflow_layout, binding, plan, |_| {})
349 }
350 fn prepare_with(
351 workflow_layout: &'a BackupLayoutGuard,
352 binding: ExecutionStageBindingRecord,
353 plan: OperationPlanRecord,
354 mut barrier: impl FnMut(StagePreparationBarrier),
355 ) -> Result<Self, ExecutionStagePreparationError> {
356 let authorities = plan.attempt_authorities()?;
357 let stage = Self::create_with(workflow_layout, binding, plan, &mut barrier)?;
358 let layout = stage.layout()?;
359 for authority in authorities {
360 let sequence = authority.binding().operation_sequence();
361 drop(AttemptJournalGuard::create(layout, authority)?);
362 barrier(StagePreparationBarrier::Journal(sequence));
363 }
364 stage.layout()?;
365 Ok(stage)
366 }
367 fn create_with(
369 workflow_layout: &'a BackupLayoutGuard,
370 binding: ExecutionStageBindingRecord,
371 plan: OperationPlanRecord,
372 mut barrier: impl FnMut(StagePreparationBarrier),
373 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
374 let workflow = read_execution_workflow(workflow_layout, binding.workflow())?;
375 binding.validate(&workflow, &plan)?;
376 let binding_bytes = super::json::encode_json(&binding, MAX_EXECUTION_STAGE_BYTES)?;
377 let plan_bytes = super::json::encode_json(
379 &plan,
380 crate::model::operation_plan::MAX_OPERATION_PLAN_BYTES,
381 )?;
382 validate_predecessors(workflow_layout, &workflow, &binding)?;
383 let path = stage_path(workflow_layout, binding.stage_sequence());
384 ic_host_fs::durable::create_private_directory(&path)
385 .map_err(|error| PersistenceError::Io(crate::ops::directory_creation_io(error)))?;
386 let stage_layout = acquire_stage(workflow_layout, binding.stage_sequence())?;
387 super::operation_plan::create_encoded(&stage_layout, &plan_bytes)?;
388 barrier(StagePreparationBarrier::Plan);
389 super::json::create_encoded(&stage_layout.root().join(BINDING_FILE), &binding_bytes)?;
390 barrier(StagePreparationBarrier::Binding);
391 workflow_layout.check_root()?;
392 Ok(Self {
393 workflow_layout,
394 stage_layout,
395 binding,
396 plan,
397 })
398 }
399 pub fn open(
408 workflow_layout: &'a BackupLayoutGuard,
409 expected_workflow: &ArtifactChecksumRecord,
410 sequence: u64,
411 expected_binding: &ArtifactChecksumRecord,
412 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
413 let workflow = read_execution_workflow(workflow_layout, expected_workflow)?;
414 workflow
415 .stage(sequence)
416 .map_err(|_| ExecutionWorkflowError::UnknownStage(sequence))?;
417 let stage_layout = acquire_stage(workflow_layout, sequence)?;
418 let (binding, plan) = read_binding(&stage_layout, &workflow, sequence, expected_binding)?;
419 validate_predecessors(workflow_layout, &workflow, &binding)?;
420 workflow_layout.check_root()?;
421 Ok(Self {
422 workflow_layout,
423 stage_layout,
424 binding,
425 plan,
426 })
427 }
428 #[must_use]
430 pub const fn plan(&self) -> &OperationPlanRecord {
431 &self.plan
432 }
433 #[must_use]
435 pub const fn binding(&self) -> &ExecutionStageBindingRecord {
436 &self.binding
437 }
438 pub fn layout(&self) -> Result<&BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
445 let workflow = read_execution_workflow(self.workflow_layout, self.binding.workflow())?;
446 read_binding(
447 &self.stage_layout,
448 &workflow,
449 self.binding.stage_sequence(),
450 &self.binding.digest(),
451 )?;
452 validate_predecessors(self.workflow_layout, &workflow, &self.binding)?;
453 Ok(&self.stage_layout)
454 }
455}
456
457#[derive(Debug, Error)]
459pub enum ExecutionStageCheckpointError {
460 #[error(transparent)]
462 Stage(#[from] ExecutionWorkflowPersistenceError),
463 #[error(transparent)]
465 Settlement(#[from] ExecutionSettlementCheckpointError),
466}
467
468#[derive(Debug, Error)]
470pub enum ExecutionStagePreparationError {
471 #[error(transparent)]
473 Stage(#[from] ExecutionWorkflowPersistenceError),
474 #[error(transparent)]
476 Authority(#[from] OperationPlanError),
477 #[error(transparent)]
479 Journal(#[from] AttemptJournalError),
480}
481
482#[derive(Debug, Error)]
484pub enum ExecutionStageResumeError {
485 #[error(transparent)]
487 Stage(#[from] ExecutionWorkflowPersistenceError),
488 #[error(transparent)]
490 Progress(#[from] ExecutionProgressPersistenceError),
491}
492fn read_binding(
493 layout: &BackupLayoutGuard,
494 workflow: &ExecutionWorkflowRecord,
495 sequence: u64,
496 expected: &ArtifactChecksumRecord,
497) -> Result<(ExecutionStageBindingRecord, OperationPlanRecord), ExecutionWorkflowPersistenceError> {
498 layout.check_root()?;
499 let binding: ExecutionStageBindingRecord =
500 read_json(&layout.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)?;
501 super::json::check_json_size(&binding, MAX_EXECUTION_STAGE_BYTES)?;
502 if binding.stage_sequence() != sequence || &binding.digest() != expected {
503 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
504 }
505 let plan = read_operation_plan(layout, binding.plan())?;
506 binding.validate(workflow, &plan)?;
507 Ok((binding, plan))
508}
509fn validate_predecessors(
510 layout: &BackupLayoutGuard,
511 workflow: &ExecutionWorkflowRecord,
512 binding: &ExecutionStageBindingRecord,
513) -> Result<(), ExecutionWorkflowPersistenceError> {
514 let mut expected: BTreeMap<u64, ExecutionStagePredecessorRecord> = binding
518 .predecessors()
519 .iter()
520 .map(|row| (row.stage_sequence(), row.clone()))
521 .collect();
522 let mut pending: Vec<_> = expected.keys().copied().collect();
523 while let Some(sequence) = pending.pop() {
524 let row = &expected[&sequence];
525 let original = {
526 let predecessor = acquire_stage(layout, sequence)?;
529 let (original, _) = read_binding(&predecessor, workflow, sequence, row.binding())?;
530 read_execution_settlement(&predecessor, original.plan(), row.settlement())?;
531 original
532 };
533 for ancestor in original.predecessors() {
534 match expected.entry(ancestor.stage_sequence()) {
535 Entry::Vacant(entry) => {
536 pending.push(ancestor.stage_sequence());
537 entry.insert(ancestor.clone());
538 }
539 Entry::Occupied(entry) => {
540 if entry.get().binding() != ancestor.binding()
541 || entry.get().settlement() != ancestor.settlement()
542 {
543 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
544 }
545 }
546 }
547 }
548 }
549 Ok(())
550}
551fn stage_path(layout: &BackupLayoutGuard, sequence: u64) -> PathBuf {
552 layout.root().join(format!("execution-stage-{sequence}"))
553}
554fn acquire_stage(
555 layout: &BackupLayoutGuard,
556 sequence: u64,
557) -> Result<BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
558 layout.check_root()?;
559 let path = stage_path(layout, sequence);
560 let metadata = fs::symlink_metadata(&path).map_err(PersistenceError::from)?;
561 if !metadata.is_dir() {
562 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
563 }
564 let stage = BackupLayoutGuard::acquire(&path)?;
565 if stage.root() != path {
568 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
569 }
570 layout.check_root()?;
571 Ok(stage)
572}
573#[derive(Debug, Error)]
575pub enum ExecutionWorkflowPersistenceError {
576 #[error("execution workflow/stage digest mismatch")]
578 DigestMismatch,
579 #[error("unsafe execution stage directory")]
581 UnsafeStage,
582 #[error(transparent)]
584 Model(#[from] ExecutionWorkflowError),
585 #[error(transparent)]
587 Plan(#[from] OperationPlanPersistenceError),
588 #[error(transparent)]
590 Settlement(#[from] ExecutionSettlementPersistenceError),
591 #[error(transparent)]
593 Lock(#[from] JournalLockError),
594 #[error(transparent)]
596 Persistence(#[from] PersistenceError),
597}
598
599#[cfg(all(test, unix))]
600mod tests;