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 for allocated in workflow.stages() {
197 let sequence = allocated.operation_sequence();
198 let stage = acquire_stage(layout, sequence)?;
199 let binding: ExecutionStageBindingRecord =
200 read_json(&stage.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)
201 .map_err(ExecutionWorkflowPersistenceError::from)?;
202 let (binding, plan) = read_binding(&stage, &workflow, sequence, &binding.digest())?;
203 let settlement = replay_execution_settlement(&stage, &plan.digest())?;
204 read_binding(&stage, &workflow, sequence, &binding.digest())?;
205 let view = ExecutionStageSettlementView {
206 stage_sequence: sequence,
207 binding: binding.digest(),
208 settlement: settlement.digest(),
209 };
210 stages.push((binding, view));
211 }
212 for (binding, _) in &stages {
213 for predecessor in binding.predecessors() {
214 let index = stages
216 .binary_search_by_key(&predecessor.stage_sequence(), |(_, view)| {
217 view.stage_sequence()
218 })
219 .map_err(|_| ExecutionWorkflowPersistenceError::DigestMismatch)?;
220 let actual = &stages[index].1;
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: stages.into_iter().map(|(_, view)| view).collect(),
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
439 #[must_use]
441 pub const fn workflow_layout(&self) -> &BackupLayoutGuard {
442 self.workflow_layout
443 }
444 pub fn layout(&self) -> Result<&BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
451 let workflow = read_execution_workflow(self.workflow_layout, self.binding.workflow())?;
452 read_binding(
453 &self.stage_layout,
454 &workflow,
455 self.binding.stage_sequence(),
456 &self.binding.digest(),
457 )?;
458 validate_predecessors(self.workflow_layout, &workflow, &self.binding)?;
459 Ok(&self.stage_layout)
460 }
461}
462
463#[derive(Debug, Error)]
465pub enum ExecutionStageCheckpointError {
466 #[error(transparent)]
468 Stage(#[from] ExecutionWorkflowPersistenceError),
469 #[error(transparent)]
471 Settlement(#[from] ExecutionSettlementCheckpointError),
472}
473
474#[derive(Debug, Error)]
476pub enum ExecutionStagePreparationError {
477 #[error(transparent)]
479 Stage(#[from] ExecutionWorkflowPersistenceError),
480 #[error(transparent)]
482 Authority(#[from] OperationPlanError),
483 #[error(transparent)]
485 Journal(#[from] AttemptJournalError),
486}
487
488#[derive(Debug, Error)]
490pub enum ExecutionStageResumeError {
491 #[error(transparent)]
493 Stage(#[from] ExecutionWorkflowPersistenceError),
494 #[error(transparent)]
496 Progress(#[from] ExecutionProgressPersistenceError),
497}
498fn read_binding(
499 layout: &BackupLayoutGuard,
500 workflow: &ExecutionWorkflowRecord,
501 sequence: u64,
502 expected: &ArtifactChecksumRecord,
503) -> Result<(ExecutionStageBindingRecord, OperationPlanRecord), ExecutionWorkflowPersistenceError> {
504 layout.check_root()?;
505 let binding: ExecutionStageBindingRecord =
506 read_json(&layout.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)?;
507 super::json::check_json_size(&binding, MAX_EXECUTION_STAGE_BYTES)?;
508 if binding.stage_sequence() != sequence || &binding.digest() != expected {
509 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
510 }
511 let plan = read_operation_plan(layout, binding.plan())?;
512 binding.validate(workflow, &plan)?;
513 Ok((binding, plan))
514}
515fn validate_predecessors(
516 layout: &BackupLayoutGuard,
517 workflow: &ExecutionWorkflowRecord,
518 binding: &ExecutionStageBindingRecord,
519) -> Result<(), ExecutionWorkflowPersistenceError> {
520 let mut expected: BTreeMap<_, _> = binding
524 .predecessors()
525 .iter()
526 .map(|row| {
527 (
528 row.stage_sequence(),
529 (row.binding().clone(), row.settlement().clone()),
530 )
531 })
532 .collect();
533 let mut pending: Vec<_> = expected.keys().copied().collect();
534 while let Some(sequence) = pending.pop() {
535 let (expected_binding, expected_settlement) = &expected[&sequence];
536 let original = {
537 let predecessor = acquire_stage(layout, sequence)?;
540 let (original, _) = read_binding(&predecessor, workflow, sequence, expected_binding)?;
541 read_execution_settlement(&predecessor, original.plan(), expected_settlement)?;
542 original
543 };
544 for ancestor in original.predecessors() {
545 match expected.entry(ancestor.stage_sequence()) {
546 Entry::Vacant(entry) => {
547 pending.push(ancestor.stage_sequence());
548 entry.insert((ancestor.binding().clone(), ancestor.settlement().clone()));
549 }
550 Entry::Occupied(entry) => {
551 let (expected_binding, expected_settlement) = entry.get();
552 if expected_binding != ancestor.binding()
553 || expected_settlement != ancestor.settlement()
554 {
555 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
556 }
557 }
558 }
559 }
560 }
561 Ok(())
562}
563fn stage_path(layout: &BackupLayoutGuard, sequence: u64) -> PathBuf {
564 layout.root().join(format!("execution-stage-{sequence}"))
565}
566fn acquire_stage(
567 layout: &BackupLayoutGuard,
568 sequence: u64,
569) -> Result<BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
570 layout.check_root()?;
571 let path = stage_path(layout, sequence);
572 let metadata = fs::symlink_metadata(&path).map_err(PersistenceError::from)?;
573 if !metadata.is_dir() {
574 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
575 }
576 let stage = BackupLayoutGuard::acquire(&path)?;
577 if stage.root() != path {
580 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
581 }
582 layout.check_root()?;
583 Ok(stage)
584}
585#[derive(Debug, Error)]
587pub enum ExecutionWorkflowPersistenceError {
588 #[error("execution workflow/stage digest mismatch")]
590 DigestMismatch,
591 #[error("unsafe execution stage directory")]
593 UnsafeStage,
594 #[error(transparent)]
596 Model(#[from] ExecutionWorkflowError),
597 #[error(transparent)]
599 Plan(#[from] OperationPlanPersistenceError),
600 #[error(transparent)]
602 Settlement(#[from] ExecutionSettlementPersistenceError),
603 #[error(transparent)]
605 Lock(#[from] JournalLockError),
606 #[error(transparent)]
608 Persistence(#[from] PersistenceError),
609}
610
611#[cfg(all(test, unix))]
612mod tests;