Skip to main content

canic_backup/journal/
types.rs

1//! Module: journal::types
2//!
3//! Responsibility: define durable artifact journal state and transitions.
4//! Does not own: persistence, resume reporting, or full journal validation.
5//! Boundary: exposes typed journal records consumed by backup workflows.
6
7use crate::journal::JournalValidationError;
8
9use serde::{Deserialize, Serialize};
10
11///
12/// DownloadJournal
13///
14/// Durable artifact download journal for one backup run.
15/// Owned by backup journaling and consumed by resume and integrity checks.
16///
17
18#[derive(Clone, Debug, Deserialize, Serialize)]
19#[serde(deny_unknown_fields)]
20pub struct DownloadJournal {
21    pub journal_version: u16,
22    pub backup_id: String,
23    pub discovery_topology_hash: String,
24    pub pre_snapshot_topology_hash: String,
25    pub operation_metrics: DownloadOperationMetrics,
26    pub artifacts: Vec<ArtifactJournalEntry>,
27}
28
29///
30/// DownloadOperationMetrics
31///
32/// Counters for artifact download lifecycle progress.
33/// Owned by backup journaling and reported in resume summaries.
34///
35
36#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
37#[serde(deny_unknown_fields)]
38pub struct DownloadOperationMetrics {
39    pub target_count: usize,
40    pub snapshot_create_started: usize,
41    pub snapshot_create_completed: usize,
42    pub snapshot_download_started: usize,
43    pub snapshot_download_completed: usize,
44    pub checksum_verify_started: usize,
45    pub checksum_verify_completed: usize,
46    pub artifact_finalize_started: usize,
47    pub artifact_finalize_completed: usize,
48}
49
50///
51/// ArtifactJournalEntry
52///
53/// Durable journal entry for one snapshot artifact.
54/// Owned by backup journaling and advanced by snapshot download workflows.
55///
56
57#[derive(Clone, Debug, Deserialize, Serialize)]
58#[serde(deny_unknown_fields)]
59pub struct ArtifactJournalEntry {
60    pub canister_id: String,
61    pub snapshot_id: String,
62    #[serde(deserialize_with = "crate::serialization::required_option")]
63    pub snapshot_taken_at_timestamp: Option<u64>,
64    #[serde(deserialize_with = "crate::serialization::required_option")]
65    pub snapshot_total_size_bytes: Option<u64>,
66    pub state: ArtifactState,
67    #[serde(deserialize_with = "crate::serialization::required_option")]
68    pub temp_path: Option<String>,
69    pub artifact_path: String,
70    pub checksum_algorithm: String,
71    #[serde(deserialize_with = "crate::serialization::required_option")]
72    pub checksum: Option<String>,
73    pub updated_at: String,
74}
75
76impl ArtifactJournalEntry {
77    /// Return the idempotent action needed to resume this artifact.
78    #[must_use]
79    pub const fn resume_action(&self) -> ResumeAction {
80        match self.state {
81            ArtifactState::Created => ResumeAction::Download,
82            ArtifactState::Downloaded => ResumeAction::VerifyChecksum,
83            ArtifactState::ChecksumVerified => ResumeAction::Finalize,
84            ArtifactState::Durable => ResumeAction::Skip,
85        }
86    }
87
88    /// Advance this artifact to a later journal state.
89    pub fn advance_to(
90        &mut self,
91        next_state: ArtifactState,
92        updated_at: String,
93    ) -> Result<(), JournalValidationError> {
94        if !self.state.can_advance_to(next_state) {
95            return Err(JournalValidationError::InvalidStateTransition {
96                from: self.state,
97                to: next_state,
98            });
99        }
100
101        self.state = next_state;
102        self.updated_at = updated_at;
103        Ok(())
104    }
105}
106
107///
108/// ArtifactState
109///
110/// Monotonic durable state for one snapshot artifact.
111/// Owned by backup journaling and used to derive idempotent resume actions.
112///
113
114#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
115pub enum ArtifactState {
116    Created,
117    Downloaded,
118    ChecksumVerified,
119    Durable,
120}
121
122impl ArtifactState {
123    /// Return whether this state can advance monotonically to `next`.
124    #[must_use]
125    pub const fn can_advance_to(self, next: Self) -> bool {
126        self.as_order() <= next.as_order()
127    }
128
129    /// Return the stable ordering used by the journal state machine.
130    const fn as_order(self) -> u8 {
131        match self {
132            Self::Created => 0,
133            Self::Downloaded => 1,
134            Self::ChecksumVerified => 2,
135            Self::Durable => 3,
136        }
137    }
138}
139
140///
141/// ResumeAction
142///
143/// Next idempotent action needed to resume an artifact download.
144/// Owned by backup journaling and derived from artifact state.
145///
146
147#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
148pub enum ResumeAction {
149    Download,
150    VerifyChecksum,
151    Finalize,
152    Skip,
153}