appcore_sync/sync/
checkpoint.rs1use crate::sync::error::SyncResult;
14use crate::sync::persistence::{
15 acquire_persistence_lock, atomic_write, read_bounded_text, split_format,
16};
17use parking_lot::Mutex;
18use std::collections::BTreeMap;
19use std::fs;
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22
23pub const SYNC_CHECKPOINT_FORMAT_V1: &str = "# appcore-sync-checkpoint-v1";
25const MAX_CHECKPOINT_FILE_BYTES: u64 = 8 * 1024 * 1024;
26const MAX_CHECKPOINT_PEER_ID_BYTES: usize = 256;
27type Checkpoint = (u64, String);
28type CheckpointMap = BTreeMap<String, Checkpoint>;
29
30pub trait SyncCheckpointStore: Send + Sync {
32 fn get_checkpoint(&self, peer_id: &str) -> SyncResult<Option<(u64, String)>>;
34 fn set_checkpoint(&self, peer_id: &str, sequence: u64, hash: &str) -> SyncResult<()>;
36
37 fn get_last_sequence(&self, peer_id: &str) -> SyncResult<u64> {
39 Ok(self
40 .get_checkpoint(peer_id)?
41 .map(|(seq, _)| seq)
42 .unwrap_or(0))
43 }
44
45 fn set_last_sequence(&self, peer_id: &str, sequence: u64) -> SyncResult<()> {
47 let hash = self
48 .get_checkpoint(peer_id)?
49 .map(|(_, h)| h)
50 .unwrap_or_default();
51 self.set_checkpoint(peer_id, sequence, &hash)
52 }
53}
54
55#[derive(Debug, Clone, Default)]
57pub struct InMemorySyncCheckpointStore {
58 checkpoints: Arc<Mutex<CheckpointMap>>,
59}
60
61impl InMemorySyncCheckpointStore {
62 pub fn new() -> Self {
64 Self {
65 checkpoints: Arc::new(Mutex::new(BTreeMap::new())),
66 }
67 }
68}
69
70impl SyncCheckpointStore for InMemorySyncCheckpointStore {
71 fn get_checkpoint(&self, peer_id: &str) -> SyncResult<Option<(u64, String)>> {
72 validate_peer_id(peer_id)?;
73 let guard = self.checkpoints.lock();
74 Ok(guard.get(peer_id).cloned())
75 }
76
77 fn set_checkpoint(&self, peer_id: &str, sequence: u64, hash: &str) -> SyncResult<()> {
78 validate_peer_id(peer_id)?;
79 validate_checkpoint_hash(hash)?;
80 let mut guard = self.checkpoints.lock();
81 guard.insert(peer_id.to_string(), (sequence, hash.to_string()));
82 Ok(())
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct FileSyncCheckpointStore {
89 file_path: PathBuf,
90 lock: Arc<Mutex<()>>,
91}
92
93impl FileSyncCheckpointStore {
94 pub fn new(file_path: impl Into<PathBuf>) -> SyncResult<Self> {
96 let file_path = file_path.into();
97 if let Some(parent) = file_path.parent() {
98 fs::create_dir_all(parent)
99 .map_err(|err| crate::sync::error::SyncError::ReplicationFailed(err.to_string()))?;
100 }
101 let _process_lock = acquire_persistence_lock(&file_path)?;
102 if !file_path.exists() {
103 atomic_write(
104 &file_path,
105 format!("{SYNC_CHECKPOINT_FORMAT_V1}\n").as_bytes(),
106 )?;
107 }
108 let store = Self {
109 file_path,
110 lock: Arc::new(Mutex::new(())),
111 };
112 store.read_state()?;
113 Ok(store)
114 }
115
116 pub fn file_path(&self) -> &Path {
118 &self.file_path
119 }
120
121 fn read_state(&self) -> SyncResult<CheckpointMap> {
122 let text = read_bounded_text(&self.file_path, MAX_CHECKPOINT_FILE_BYTES)?;
123 let formatted = split_format(&text, SYNC_CHECKPOINT_FORMAT_V1)?;
124 parse_checkpoint_map(formatted.body)
125 }
126
127 fn read_map(&self) -> SyncResult<CheckpointMap> {
128 self.read_state()
129 }
130
131 fn write_map(&self, map: &CheckpointMap) -> SyncResult<()> {
132 let mut out = format!("{SYNC_CHECKPOINT_FORMAT_V1}\n");
133 for (peer_id, (sequence, hash)) in map {
134 out.push_str(peer_id);
135 out.push('=');
136 out.push_str(&sequence.to_string());
137 out.push(',');
138 out.push_str(hash);
139 out.push('\n');
140 }
141 atomic_write(&self.file_path, out.as_bytes())
142 }
143}
144
145impl SyncCheckpointStore for FileSyncCheckpointStore {
146 fn get_checkpoint(&self, peer_id: &str) -> SyncResult<Option<(u64, String)>> {
147 validate_peer_id(peer_id)?;
148 let _guard = self.lock.lock();
149 let _process_lock = acquire_persistence_lock(&self.file_path)?;
150 let map = self.read_map()?;
151 Ok(map.get(peer_id).cloned())
152 }
153
154 fn set_checkpoint(&self, peer_id: &str, sequence: u64, hash: &str) -> SyncResult<()> {
155 validate_peer_id(peer_id)?;
156 validate_checkpoint_hash(hash)?;
157 let _guard = self.lock.lock();
158 let _process_lock = acquire_persistence_lock(&self.file_path)?;
159 let mut map = self.read_map()?;
160 map.insert(peer_id.to_string(), (sequence, hash.to_string()));
161 self.write_map(&map)
162 }
163}
164
165fn validate_peer_id(peer_id: &str) -> SyncResult<()> {
166 if peer_id.is_empty() || peer_id.len() > MAX_CHECKPOINT_PEER_ID_BYTES {
167 return Err(crate::sync::error::SyncError::InvalidPeerId);
168 }
169 if !peer_id
170 .chars()
171 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | ':' | '-'))
172 {
173 return Err(crate::sync::error::SyncError::InvalidPeerId);
174 }
175 Ok(())
176}
177
178fn validate_checkpoint_hash(hash: &str) -> SyncResult<()> {
179 if hash.is_empty() || (hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit())) {
180 return Ok(());
181 }
182 Err(crate::sync::error::SyncError::ReplicationFailed(
183 "invalid checkpoint hash".to_string(),
184 ))
185}
186
187fn parse_checkpoint_map(text: &str) -> SyncResult<CheckpointMap> {
188 let mut map = BTreeMap::new();
189 for line in text.lines().filter(|line| !line.trim().is_empty()) {
190 let (peer_id, rest) = line.split_once('=').ok_or_else(|| {
191 crate::sync::error::SyncError::ReplicationFailed("invalid checkpoint line".to_string())
192 })?;
193 validate_peer_id(peer_id)?;
194
195 let (sequence_text, hash) = if let Some((seq_t, h_t)) = rest.split_once(',') {
196 (seq_t, h_t.to_string())
197 } else {
198 (rest, "".to_string())
199 };
200
201 let sequence = sequence_text.parse::<u64>().map_err(|_| {
202 crate::sync::error::SyncError::ReplicationFailed(
203 "invalid checkpoint sequence".to_string(),
204 )
205 })?;
206 validate_checkpoint_hash(&hash)?;
207 map.insert(peer_id.to_string(), (sequence, hash));
208 }
209 Ok(map)
210}