Skip to main content

syncweb_core/sync/
checkpoint.rs

1use std::collections::HashSet;
2
3use iroh_blobs::Hash;
4use iroh_docs::NamespaceId;
5
6use crate::{Result, storage::node_db::NodeDatabase};
7
8/// Manages sync session checkpointing for crash recovery.
9///
10/// Tracks which entries have been processed, failed, or skipped during a sync
11/// session so that interrupted syncs can resume without re-downloading
12/// already-transferred blobs.
13#[derive(Clone, Debug)]
14pub struct SyncCheckpoint {
15    namespace_id: NamespaceId,
16    session_id: String,
17    database: NodeDatabase,
18}
19
20/// Progress for a single entry within a sync session.
21#[derive(Clone, Debug)]
22#[non_exhaustive]
23pub struct EntryProgress {
24    pub entry_key: Vec<u8>,
25    pub hash: Hash,
26    pub size: u64,
27    pub status: String,
28    pub retries: u32,
29    pub error_message: Option<String>,
30}
31
32/// Aggregate progress for a sync session.
33#[derive(Clone, Copy, Debug)]
34#[non_exhaustive]
35pub struct CheckpointProgress {
36    pub total: usize,
37    pub completed: usize,
38    pub failed: usize,
39    pub skipped: usize,
40    pub pending: usize,
41    pub bytes_transferred: u64,
42    pub bytes_total: Option<u64>,
43    pub percentage: f64,
44}
45
46impl SyncCheckpoint {
47    /// Create a new checkpoint manager for the given namespace.
48    #[must_use]
49    pub fn new(database: &NodeDatabase, namespace_id: NamespaceId) -> Self {
50        let session_id = uuid::Uuid::new_v4().to_string();
51        Self {
52            namespace_id,
53            session_id,
54            database: database.clone(),
55        }
56    }
57
58    /// The current session ID.
59    #[must_use]
60    pub fn session_id(&self) -> &str {
61        &self.session_id
62    }
63
64    /// The namespace ID for this checkpoint.
65    #[must_use]
66    pub const fn namespace_id(&self) -> NamespaceId {
67        self.namespace_id
68    }
69
70    /// Create a new sync session in the database and return the checkpoint.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if the database write fails.
75    pub fn create_session(database: &NodeDatabase, namespace_id: NamespaceId) -> Result<Self> {
76        let cp = Self::new(database, namespace_id);
77        database.create_sync_checkpoint(&cp.namespace_id.to_string(), &cp.session_id)?;
78        Ok(cp)
79    }
80
81    /// Mark an entry as completed (downloaded successfully).
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the database write fails.
86    pub fn mark_completed(&self, entry_key: &[u8], hash: Hash, size: u64) -> Result<()> {
87        self.database
88            .upsert_sync_entry(&crate::storage::node_db::SyncEntryParams {
89                namespace_id: &self.namespace_id.to_string(),
90                session_id: &self.session_id,
91                entry_key,
92                hash: &hash.to_string().into_bytes(),
93                size,
94                status: "completed",
95                retries: 0,
96                error_message: None,
97            })
98    }
99
100    /// Mark an entry as failed with error message.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if the database write fails.
105    pub fn mark_failed(&self, entry_key: &[u8], error: &str) -> Result<()> {
106        self.database
107            .upsert_sync_entry(&crate::storage::node_db::SyncEntryParams {
108                namespace_id: &self.namespace_id.to_string(),
109                session_id: &self.session_id,
110                entry_key,
111                hash: &[],
112                size: 0,
113                status: "failed",
114                retries: 0,
115                error_message: Some(error),
116            })
117    }
118
119    /// Mark an entry as skipped (already present locally or deleted).
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if the database write fails.
124    pub fn mark_skipped(&self, entry_key: &[u8], hash: Hash, size: u64) -> Result<()> {
125        self.database
126            .upsert_sync_entry(&crate::storage::node_db::SyncEntryParams {
127                namespace_id: &self.namespace_id.to_string(),
128                session_id: &self.session_id,
129                entry_key,
130                hash: &hash.to_string().into_bytes(),
131                size,
132                status: "skipped",
133                retries: 0,
134                error_message: None,
135            })
136    }
137
138    /// Get all completed entries for this session (for resume filtering).
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if the database query fails.
143    pub fn completed_entries(&self) -> Result<Vec<EntryProgress>> {
144        self.database
145            .list_sync_entries(&self.namespace_id.to_string(), &self.session_id, "completed")
146    }
147
148    /// Get all failed entries for this session (for retry).
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the database query fails.
153    pub fn failed_entries(&self) -> Result<Vec<EntryProgress>> {
154        self.database
155            .list_sync_entries(&self.namespace_id.to_string(), &self.session_id, "failed")
156    }
157
158    /// Get all completed entry keys as a `HashSet` for fast lookup.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the database query fails.
163    pub fn completed_keys(&self) -> Result<HashSet<Vec<u8>>> {
164        let entries = self.completed_entries()?;
165        Ok(entries.into_iter().map(|e| e.entry_key).collect())
166    }
167
168    /// Get overall progress.
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if the database query fails.
173    pub fn progress(&self) -> Result<CheckpointProgress> {
174        self.database
175            .get_checkpoint_progress(&self.namespace_id.to_string(), &self.session_id)
176    }
177
178    /// Mark session as completed.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the database write fails.
183    pub fn complete(&self) -> Result<()> {
184        self.database
185            .update_checkpoint_status(&self.namespace_id.to_string(), &self.session_id, "completed")
186    }
187
188    /// Mark session as incomplete (has leftovers for next resume).
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the database write fails.
193    pub const fn mark_incomplete(&self) -> Result<()> {
194        // Leave the status as 'running' so it's picked up on resume
195        Ok(())
196    }
197
198    /// Delete the checkpoint and its entries.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the database write fails.
203    pub fn delete(&self) -> Result<()> {
204        self.database
205            .delete_sync_checkpoint(&self.namespace_id.to_string(), &self.session_id)
206    }
207
208    /// Load the most recent unfinished checkpoint for a folder.
209    ///
210    /// Returns `None` if no unfinished checkpoint exists.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if the database query fails.
215    pub fn resume(database: &NodeDatabase, namespace_id: NamespaceId) -> Result<Option<Self>> {
216        let ns = namespace_id.to_string();
217        Ok(database.find_unfinished_checkpoint(&ns)?.map(|sid| Self {
218            namespace_id,
219            session_id: sid,
220            database: database.clone(),
221        }))
222    }
223}