1use super::{
4 AttemptJournalError, AttemptJournalGuard, BackupLayoutGuard, ExecutionProgressPersistenceError,
5 ExecutionSettlementCheckpointError, ExecutionSettlementPersistenceError, JournalLock,
6 JournalLockError, OperationPlanPersistenceError, PersistenceError,
7 checkpoint_execution_settlement, create_json_durable, create_operation_plan,
8 read_execution_progress, read_execution_settlement, read_json, read_operation_plan,
9 reconcile_execution_settlement, replay_execution_settlement,
10};
11use crate::model::{
12 artifacts::ArtifactChecksumRecord,
13 execution_workflow::{
14 ExecutionStageBindingRecord, ExecutionStagePredecessorRecord, ExecutionWorkflowError,
15 ExecutionWorkflowRecord, MAX_EXECUTION_STAGE_BYTES, MAX_EXECUTION_WORKFLOW_BYTES,
16 },
17 operation_plan::{OperationPlanError, OperationPlanRecord},
18};
19use crate::policy::execution_progress::ExecutionProgressView;
20use std::{
21 collections::{BTreeMap, BTreeSet, btree_map::Entry},
22 fs,
23 path::PathBuf,
24};
25use thiserror::Error;
26
27const WORKFLOW_FILE: &str = "execution-workflow.json";
28const BINDING_FILE: &str = "stage-binding.json";
29
30#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct ExecutionWorkflowSettlementView {
37 workflow: ArtifactChecksumRecord,
38 stages: Vec<ExecutionStageSettlementView>,
39}
40impl ExecutionWorkflowSettlementView {
41 #[must_use]
43 pub const fn workflow(&self) -> &ArtifactChecksumRecord {
44 &self.workflow
45 }
46 #[must_use]
48 pub fn stages(&self) -> &[ExecutionStageSettlementView] {
49 &self.stages
50 }
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct ExecutionStageSettlementView {
56 stage_sequence: u64,
57 binding: ArtifactChecksumRecord,
58 settlement: ArtifactChecksumRecord,
59}
60impl ExecutionStageSettlementView {
61 #[must_use]
63 pub const fn stage_sequence(&self) -> u64 {
64 self.stage_sequence
65 }
66 #[must_use]
68 pub const fn binding(&self) -> &ArtifactChecksumRecord {
69 &self.binding
70 }
71 #[must_use]
73 pub const fn settlement(&self) -> &ArtifactChecksumRecord {
74 &self.settlement
75 }
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79enum StagePreparationBarrier {
80 Plan,
81 Binding,
82 Journal(u64),
83}
84
85pub fn create_execution_workflow(
89 layout: &BackupLayoutGuard,
90 record: &ExecutionWorkflowRecord,
91) -> Result<(), ExecutionWorkflowPersistenceError> {
92 layout.check_root()?;
93 super::json::check_json_size(record, MAX_EXECUTION_WORKFLOW_BYTES)?;
94 let path = layout.root().join(WORKFLOW_FILE);
95 let _lock = JournalLock::acquire(&path)?;
96 create_json_durable(&path, record)?;
97 Ok(())
98}
99pub fn read_execution_workflow(
103 layout: &BackupLayoutGuard,
104 expected: &ArtifactChecksumRecord,
105) -> Result<ExecutionWorkflowRecord, ExecutionWorkflowPersistenceError> {
106 layout.check_root()?;
107 let path = layout.root().join(WORKFLOW_FILE);
108 let _lock = JournalLock::acquire(&path)?;
109 let record: ExecutionWorkflowRecord = read_json(&path, MAX_EXECUTION_WORKFLOW_BYTES)?;
110 super::json::check_json_size(&record, MAX_EXECUTION_WORKFLOW_BYTES)?;
111 if &record.digest() != expected {
112 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
113 }
114 Ok(record)
115}
116
117pub(crate) fn read_execution_stage_occupancy(
125 layout: &BackupLayoutGuard,
126 expected: &ArtifactChecksumRecord,
127) -> Result<BTreeSet<u64>, ExecutionWorkflowPersistenceError> {
128 let workflow = read_execution_workflow(layout, expected)?;
129 let mut occupied = BTreeSet::new();
130 for stage in workflow.stages() {
131 let sequence = stage.operation_sequence();
132 match fs::symlink_metadata(stage_path(layout, sequence)) {
133 Ok(metadata) if metadata.is_dir() => {
134 occupied.insert(sequence);
135 }
136 Ok(_) => return Err(ExecutionWorkflowPersistenceError::UnsafeStage),
137 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
138 Err(error) => return Err(PersistenceError::from(error).into()),
139 }
140 }
141 read_execution_workflow(layout, expected)?;
142 Ok(occupied)
143}
144
145pub fn read_complete_execution_workflow(
158 layout: &BackupLayoutGuard,
159 expected: &ArtifactChecksumRecord,
160) -> Result<ExecutionWorkflowSettlementView, ExecutionStageCheckpointError> {
161 let workflow = read_execution_workflow(layout, expected)?;
162 let mut stages = Vec::with_capacity(workflow.stages().len());
163 let mut bindings = Vec::with_capacity(workflow.stages().len());
164 let mut by_sequence = BTreeMap::new();
165 for allocated in workflow.stages() {
166 let sequence = allocated.operation_sequence();
167 let stage = acquire_stage(layout, sequence)?;
168 let binding: ExecutionStageBindingRecord =
169 read_json(&stage.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)
170 .map_err(ExecutionWorkflowPersistenceError::from)?;
171 let (binding, plan) = read_binding(&stage, &workflow, sequence, &binding.digest())?;
172 let settlement = replay_execution_settlement(&stage, &plan.digest())?;
173 read_binding(&stage, &workflow, sequence, &binding.digest())?;
174 by_sequence.insert(sequence, stages.len());
175 stages.push(ExecutionStageSettlementView {
176 stage_sequence: sequence,
177 binding: binding.digest(),
178 settlement: settlement.digest(),
179 });
180 bindings.push(binding);
181 }
182 for binding in &bindings {
183 for predecessor in binding.predecessors() {
184 let Some(&index) = by_sequence.get(&predecessor.stage_sequence()) else {
185 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
186 };
187 let actual = &stages[index];
188 if predecessor.binding() != actual.binding()
189 || predecessor.settlement() != actual.settlement()
190 {
191 return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
192 }
193 }
194 }
195 read_execution_workflow(layout, expected)?;
196 Ok(ExecutionWorkflowSettlementView {
197 workflow: expected.clone(),
198 stages,
199 })
200}
201
202#[derive(Debug)]
210pub struct ExecutionStageGuard<'a> {
211 workflow_layout: &'a BackupLayoutGuard,
212 stage_layout: BackupLayoutGuard,
213 binding: ExecutionStageBindingRecord,
214 plan: OperationPlanRecord,
215}
216impl<'a> ExecutionStageGuard<'a> {
217 pub fn reconcile_checkpoint(
227 &self,
228 learned_evidence: ArtifactChecksumRecord,
229 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
230 let settlement = reconcile_execution_settlement(self.layout()?, &self.plan.digest())?;
231 self.layout()?;
232 Ok(ExecutionStagePredecessorRecord::new(
233 self.binding.stage_sequence(),
234 self.binding.digest(),
235 settlement.digest(),
236 learned_evidence,
237 ))
238 }
239 pub fn replay_checkpoint(
248 &self,
249 learned_evidence: ArtifactChecksumRecord,
250 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
251 let settlement = replay_execution_settlement(self.layout()?, &self.plan.digest())?;
252 self.layout()?;
253 Ok(ExecutionStagePredecessorRecord::new(
254 self.binding.stage_sequence(),
255 self.binding.digest(),
256 settlement.digest(),
257 learned_evidence,
258 ))
259 }
260 pub fn checkpoint(
273 &self,
274 learned_evidence: ArtifactChecksumRecord,
275 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
276 self.checkpoint_with(learned_evidence, || {})
277 }
278 fn checkpoint_with(
279 &self,
280 learned_evidence: ArtifactChecksumRecord,
281 after_publication: impl FnOnce(),
282 ) -> Result<ExecutionStagePredecessorRecord, ExecutionStageCheckpointError> {
283 let settlement = checkpoint_execution_settlement(self.layout()?, &self.plan.digest())?;
284 after_publication();
285 self.layout()?;
286 Ok(ExecutionStagePredecessorRecord::new(
287 self.binding.stage_sequence(),
288 self.binding.digest(),
289 settlement.digest(),
290 learned_evidence,
291 ))
292 }
293 pub fn resume(
306 workflow_layout: &'a BackupLayoutGuard,
307 expected_workflow: &ArtifactChecksumRecord,
308 sequence: u64,
309 expected_binding: &ArtifactChecksumRecord,
310 ) -> Result<(Self, ExecutionProgressView), ExecutionStageResumeError> {
311 let stage = Self::open(
312 workflow_layout,
313 expected_workflow,
314 sequence,
315 expected_binding,
316 )?;
317 let progress = read_execution_progress(&stage.stage_layout, &stage.plan.digest())?;
318 stage.layout()?;
319 Ok((stage, progress))
320 }
321 pub fn prepare(
336 workflow_layout: &'a BackupLayoutGuard,
337 binding: ExecutionStageBindingRecord,
338 plan: OperationPlanRecord,
339 ) -> Result<Self, ExecutionStagePreparationError> {
340 Self::prepare_with(workflow_layout, binding, plan, |_| {})
341 }
342 fn prepare_with(
343 workflow_layout: &'a BackupLayoutGuard,
344 binding: ExecutionStageBindingRecord,
345 plan: OperationPlanRecord,
346 mut barrier: impl FnMut(StagePreparationBarrier),
347 ) -> Result<Self, ExecutionStagePreparationError> {
348 let authorities = plan.attempt_authorities()?;
349 let stage = Self::create_with(workflow_layout, binding, plan, &mut barrier)?;
350 let layout = stage.layout()?;
351 for authority in authorities {
352 let sequence = authority.binding().operation_sequence();
353 drop(AttemptJournalGuard::create(layout, authority)?);
354 barrier(StagePreparationBarrier::Journal(sequence));
355 }
356 stage.layout()?;
357 Ok(stage)
358 }
359 pub fn create(
368 workflow_layout: &'a BackupLayoutGuard,
369 binding: ExecutionStageBindingRecord,
370 plan: OperationPlanRecord,
371 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
372 Self::create_with(workflow_layout, binding, plan, |_| {})
373 }
374 fn create_with(
375 workflow_layout: &'a BackupLayoutGuard,
376 binding: ExecutionStageBindingRecord,
377 plan: OperationPlanRecord,
378 mut barrier: impl FnMut(StagePreparationBarrier),
379 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
380 let workflow = read_execution_workflow(workflow_layout, binding.workflow())?;
381 binding.validate(&workflow, &plan)?;
382 super::json::check_json_size(&binding, MAX_EXECUTION_STAGE_BYTES)?;
383 super::json::check_json_size(
385 &plan,
386 crate::model::operation_plan::MAX_OPERATION_PLAN_BYTES,
387 )?;
388 validate_predecessors(workflow_layout, &workflow, &binding)?;
389 let path = stage_path(workflow_layout, binding.stage_sequence());
390 create_private_directory(&path)?;
391 fs::File::open(workflow_layout.root())
392 .and_then(|directory| directory.sync_all())
393 .map_err(PersistenceError::from)?;
394 let stage_layout = acquire_stage(workflow_layout, binding.stage_sequence())?;
395 create_operation_plan(&stage_layout, &plan)?;
396 barrier(StagePreparationBarrier::Plan);
397 create_json_durable(&stage_layout.root().join(BINDING_FILE), &binding)?;
398 barrier(StagePreparationBarrier::Binding);
399 workflow_layout.check_root()?;
400 Ok(Self {
401 workflow_layout,
402 stage_layout,
403 binding,
404 plan,
405 })
406 }
407 pub fn open(
416 workflow_layout: &'a BackupLayoutGuard,
417 expected_workflow: &ArtifactChecksumRecord,
418 sequence: u64,
419 expected_binding: &ArtifactChecksumRecord,
420 ) -> Result<Self, ExecutionWorkflowPersistenceError> {
421 let workflow = read_execution_workflow(workflow_layout, expected_workflow)?;
422 workflow
423 .stage(sequence)
424 .map_err(|_| ExecutionWorkflowError::UnknownStage(sequence))?;
425 let stage_layout = acquire_stage(workflow_layout, sequence)?;
426 let (binding, plan) = read_binding(&stage_layout, &workflow, sequence, expected_binding)?;
427 validate_predecessors(workflow_layout, &workflow, &binding)?;
428 workflow_layout.check_root()?;
429 Ok(Self {
430 workflow_layout,
431 stage_layout,
432 binding,
433 plan,
434 })
435 }
436 #[must_use]
438 pub const fn plan(&self) -> &OperationPlanRecord {
439 &self.plan
440 }
441 #[must_use]
443 pub const fn binding(&self) -> &ExecutionStageBindingRecord {
444 &self.binding
445 }
446 pub fn layout(&self) -> Result<&BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
453 let workflow = read_execution_workflow(self.workflow_layout, self.binding.workflow())?;
454 read_binding(
455 &self.stage_layout,
456 &workflow,
457 self.binding.stage_sequence(),
458 &self.binding.digest(),
459 )?;
460 validate_predecessors(self.workflow_layout, &workflow, &self.binding)?;
461 Ok(&self.stage_layout)
462 }
463}
464
465#[derive(Debug, Error)]
467pub enum ExecutionStageCheckpointError {
468 #[error(transparent)]
470 Stage(#[from] ExecutionWorkflowPersistenceError),
471 #[error(transparent)]
473 Settlement(#[from] ExecutionSettlementCheckpointError),
474}
475
476#[derive(Debug, Error)]
478pub enum ExecutionStagePreparationError {
479 #[error(transparent)]
481 Stage(#[from] ExecutionWorkflowPersistenceError),
482 #[error(transparent)]
484 Authority(#[from] OperationPlanError),
485 #[error(transparent)]
487 Journal(#[from] AttemptJournalError),
488}
489
490#[derive(Debug, Error)]
492pub enum ExecutionStageResumeError {
493 #[error(transparent)]
495 Stage(#[from] ExecutionWorkflowPersistenceError),
496 #[error(transparent)]
498 Progress(#[from] ExecutionProgressPersistenceError),
499}
500fn read_binding(
501 layout: &BackupLayoutGuard,
502 workflow: &ExecutionWorkflowRecord,
503 sequence: u64,
504 expected: &ArtifactChecksumRecord,
505) -> Result<(ExecutionStageBindingRecord, OperationPlanRecord), ExecutionWorkflowPersistenceError> {
506 layout.check_root()?;
507 let binding: ExecutionStageBindingRecord =
508 read_json(&layout.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)?;
509 super::json::check_json_size(&binding, MAX_EXECUTION_STAGE_BYTES)?;
510 if binding.stage_sequence() != sequence || &binding.digest() != expected {
511 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
512 }
513 let plan = read_operation_plan(layout, binding.plan())?;
514 binding.validate(workflow, &plan)?;
515 Ok((binding, plan))
516}
517fn validate_predecessors(
518 layout: &BackupLayoutGuard,
519 workflow: &ExecutionWorkflowRecord,
520 binding: &ExecutionStageBindingRecord,
521) -> Result<(), ExecutionWorkflowPersistenceError> {
522 let mut expected: BTreeMap<u64, ExecutionStagePredecessorRecord> = binding
526 .predecessors()
527 .iter()
528 .map(|row| (row.stage_sequence(), row.clone()))
529 .collect();
530 let mut pending: Vec<_> = expected.keys().copied().collect();
531 while let Some(sequence) = pending.pop() {
532 let row = &expected[&sequence];
533 let original = {
534 let predecessor = acquire_stage(layout, sequence)?;
537 let (original, _) = read_binding(&predecessor, workflow, sequence, row.binding())?;
538 read_execution_settlement(&predecessor, original.plan(), row.settlement())?;
539 original
540 };
541 for ancestor in original.predecessors() {
542 match expected.entry(ancestor.stage_sequence()) {
543 Entry::Vacant(entry) => {
544 pending.push(ancestor.stage_sequence());
545 entry.insert(ancestor.clone());
546 }
547 Entry::Occupied(entry) => {
548 if entry.get().binding() != ancestor.binding()
549 || entry.get().settlement() != ancestor.settlement()
550 {
551 return Err(ExecutionWorkflowPersistenceError::DigestMismatch);
552 }
553 }
554 }
555 }
556 }
557 Ok(())
558}
559fn stage_path(layout: &BackupLayoutGuard, sequence: u64) -> PathBuf {
560 layout.root().join(format!("execution-stage-{sequence}"))
561}
562fn acquire_stage(
563 layout: &BackupLayoutGuard,
564 sequence: u64,
565) -> Result<BackupLayoutGuard, ExecutionWorkflowPersistenceError> {
566 layout.check_root()?;
567 let path = stage_path(layout, sequence);
568 let metadata = fs::symlink_metadata(&path).map_err(PersistenceError::from)?;
569 if !metadata.is_dir() {
570 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
571 }
572 let stage = BackupLayoutGuard::acquire(&path)?;
573 if stage.root() != path {
576 return Err(ExecutionWorkflowPersistenceError::UnsafeStage);
577 }
578 layout.check_root()?;
579 Ok(stage)
580}
581fn create_private_directory(
582 path: &std::path::Path,
583) -> Result<(), ExecutionWorkflowPersistenceError> {
584 #[cfg(unix)]
585 {
586 use std::os::unix::fs::DirBuilderExt;
587 fs::DirBuilder::new()
588 .mode(0o700)
589 .create(path)
590 .map_err(PersistenceError::from)?;
591 Ok(())
592 }
593 #[cfg(not(unix))]
594 {
595 let _ = path;
596 Err(PersistenceError::Io(std::io::Error::from(std::io::ErrorKind::Unsupported)).into())
597 }
598}
599
600#[derive(Debug, Error)]
602pub enum ExecutionWorkflowPersistenceError {
603 #[error("execution workflow/stage digest mismatch")]
605 DigestMismatch,
606 #[error("unsafe execution stage directory")]
608 UnsafeStage,
609 #[error(transparent)]
611 Model(#[from] ExecutionWorkflowError),
612 #[error(transparent)]
614 Plan(#[from] OperationPlanPersistenceError),
615 #[error(transparent)]
617 Settlement(#[from] ExecutionSettlementPersistenceError),
618 #[error(transparent)]
620 Lock(#[from] JournalLockError),
621 #[error(transparent)]
623 Persistence(#[from] PersistenceError),
624}
625
626#[cfg(all(test, unix))]
627mod tests;