1use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, SystemTime, UNIX_EPOCH};
16
17use edgestore::replication::ReplicationProtocol;
18use edgestore::{Engine, ImportResult, RemoteStore};
19
20use crate::http_client::HttpReplicationClient;
21
22#[derive(serde::Serialize, serde::Deserialize, Default)]
24pub struct PeerCursor {
25 pub last_known_merkle_root: Vec<u8>,
27 pub segments_pending: Vec<Vec<u8>>,
29 pub last_attempt_secs: u64,
31 pub segments_applied_total: u64,
33}
34
35pub struct AntiEntropyLoop {
41 engine: Arc<Mutex<Engine>>,
42 peer_url: String,
43 peer_id: String,
44 db_path: PathBuf,
45 pub interval_secs: u64,
47 remote_store: Option<Arc<dyn RemoteStore>>,
51}
52
53impl AntiEntropyLoop {
54 pub fn new(
61 engine: Arc<Mutex<Engine>>,
62 peer_url: String,
63 peer_id: String,
64 db_path: PathBuf,
65 ) -> Self {
66 AntiEntropyLoop {
67 engine,
68 peer_url,
69 peer_id,
70 db_path,
71 interval_secs: 30,
72 remote_store: None,
73 }
74 }
75
76 pub fn with_remote_store(mut self, store: Arc<dyn RemoteStore>) -> Self {
80 self.remote_store = Some(store);
81 self
82 }
83
84 pub fn with_interval(mut self, secs: u64) -> Self {
88 self.interval_secs = secs;
89 self
90 }
91
92 pub fn start(self) -> std::thread::JoinHandle<()> {
97 std::thread::spawn(move || loop {
98 std::thread::sleep(Duration::from_secs(self.interval_secs));
99 run_once(
100 &self.engine,
101 &self.peer_url,
102 &self.peer_id,
103 &self.db_path,
104 self.remote_store.as_deref(),
105 );
106 })
107 }
108}
109
110fn run_once(
112 engine: &Arc<Mutex<Engine>>,
113 peer_url: &str,
114 peer_id: &str,
115 db_path: &Path,
116 remote_store: Option<&dyn RemoteStore>,
117) {
118 let cursor_path = cursor_file_path(db_path, peer_id);
120 let mut cursor = load_cursor(&cursor_path);
121
122 cursor.last_attempt_secs = now_secs();
124 if let Err(e) = flush_cursor(&cursor, &cursor_path) {
125 eprintln!("[anti_entropy] cursor flush error: {}", e);
126 }
127
128 let client = HttpReplicationClient::new(peer_url);
130
131 let peer_root = match client.merkle_root() {
132 Ok(r) => r,
133 Err(e) => {
134 eprintln!("[anti_entropy] peer {} merkle_root error: {}", peer_id, e);
135 return;
136 }
137 };
138
139 let in_sync = {
141 match engine.lock() {
142 Ok(eng) => match eng.compare_merkle(&peer_root) {
143 Ok(same) => same,
144 Err(e) => {
145 eprintln!("[anti_entropy] compare_merkle error: {}", e);
146 return;
147 }
148 },
149 Err(_) => {
150 eprintln!("[anti_entropy] engine lock poisoned");
151 return;
152 }
153 }
154 };
155
156 if in_sync {
157 cursor.last_known_merkle_root = peer_root.to_vec();
159 if let Err(e) = flush_cursor(&cursor, &cursor_path) {
160 eprintln!("[anti_entropy] cursor flush (in-sync) error: {}", e);
161 }
162 return;
163 }
164
165 let peer_segments = match client.list_segments() {
167 Ok(segs) => segs,
168 Err(e) => {
169 eprintln!("[anti_entropy] peer {} list_segments error: {}", peer_id, e);
170 return;
171 }
172 };
173
174 let missing: Vec<[u8; 32]> = {
176 match engine.lock() {
177 Ok(eng) => eng.missing_segments(&peer_segments),
178 Err(_) => {
179 eprintln!("[anti_entropy] engine lock poisoned (missing_segments)");
180 return;
181 }
182 }
183 };
184
185 cursor.segments_pending = missing.iter().map(|h| h.to_vec()).collect();
187 if let Err(e) = flush_cursor(&cursor, &cursor_path) {
188 eprintln!("[anti_entropy] cursor flush (pending) error: {}", e);
189 }
190
191 let pending_hashes: Vec<Vec<u8>> = cursor.segments_pending.clone();
193 for hash_vec in &pending_hashes {
194 if hash_vec.len() != 32 {
195 eprintln!(
196 "[anti_entropy] skipping malformed hash (len={})",
197 hash_vec.len()
198 );
199 continue;
200 }
201
202 let mut hash = [0u8; 32];
203 hash.copy_from_slice(hash_vec);
204
205 let data = match client.fetch_segment(&hash) {
207 Ok(d) => d,
208 Err(e) => {
209 eprintln!("[anti_entropy] fetch_segment error: {}", e);
210 continue;
211 }
212 };
213
214 let result = {
216 match engine.lock() {
217 Ok(mut eng) => eng.import_segment(&data, &hash),
218 Err(_) => {
219 eprintln!("[anti_entropy] engine lock poisoned (import_segment)");
220 continue;
221 }
222 }
223 };
224
225 match result {
226 Ok(ImportResult::Applied {
227 keys_written,
228 keys_skipped,
229 }) => {
230 cursor.segments_pending.retain(|h| h != hash_vec);
232 cursor.segments_applied_total += 1;
233 if let Err(e) = flush_cursor(&cursor, &cursor_path) {
234 eprintln!("[anti_entropy] cursor flush (applied) error: {}", e);
235 }
236 eprintln!(
237 "[anti_entropy] applied segment {}: {} written, {} skipped",
238 hex_str(&hash),
239 keys_written,
240 keys_skipped
241 );
242
243 if let Some(rs) = remote_store {
245 if let Err(e) = rs.upload(&hash, &data) {
246 eprintln!(
247 "[anti_entropy] remote_store upload warning for {}: {}",
248 hex_str(&hash),
249 e
250 );
251 }
252 }
253 }
254 Ok(ImportResult::Skipped) => {
255 cursor.segments_pending.retain(|h| h != hash_vec);
257 cursor.segments_applied_total += 1;
258 if let Err(e) = flush_cursor(&cursor, &cursor_path) {
259 eprintln!("[anti_entropy] cursor flush (skipped) error: {}", e);
260 }
261 }
262 Ok(ImportResult::HashMismatch) => {
263 eprintln!(
265 "[anti_entropy] BLAKE3 mismatch for segment {} — will retry",
266 hex_str(&hash)
267 );
268 }
269 Err(e) => {
270 eprintln!("[anti_entropy] import_segment error: {}", e);
271 }
273 }
274 }
275
276 cursor.last_known_merkle_root = peer_root.to_vec();
278 if let Err(e) = flush_cursor(&cursor, &cursor_path) {
279 eprintln!("[anti_entropy] cursor flush (final) error: {}", e);
280 }
281}
282
283fn cursor_file_path(db_path: &Path, peer_id: &str) -> PathBuf {
285 db_path.join("sync").join(format!("{}.cursor", peer_id))
286}
287
288fn load_cursor(cursor_path: &Path) -> PeerCursor {
292 match std::fs::File::open(cursor_path) {
293 Ok(file) => rmp_serde::from_read(file).unwrap_or_default(),
294 Err(_) => PeerCursor::default(),
295 }
296}
297
298fn flush_cursor(cursor: &PeerCursor, cursor_path: &Path) -> Result<(), std::io::Error> {
302 if let Some(parent) = cursor_path.parent() {
304 std::fs::create_dir_all(parent)?;
305 }
306
307 let tmp_path = cursor_path.with_extension("cursor.tmp");
308
309 let bytes = rmp_serde::to_vec(cursor).map_err(|e| std::io::Error::other(e.to_string()))?;
310
311 std::fs::write(&tmp_path, &bytes)?;
312 std::fs::rename(&tmp_path, cursor_path)?;
313
314 Ok(())
315}
316
317fn now_secs() -> u64 {
319 SystemTime::now()
320 .duration_since(UNIX_EPOCH)
321 .unwrap_or_default()
322 .as_secs()
323}
324
325fn hex_str(hash: &[u8; 32]) -> String {
327 hash.iter().map(|b| format!("{:02x}", b)).collect()
328}