1use crate::sync::error::{SyncError, SyncResult};
14use crate::sync::log::{
15 replication_record_hash, validate_log_index, validate_page_limits, validate_record_count,
16 validate_record_size, ReplicationLog, MAX_REPLICATION_LOG_BYTES, MAX_REPLICATION_PAGE_BYTES,
17 MAX_REPLICATION_PAGE_RECORDS, REPLICATION_LOG_FORMAT_V1,
18};
19use crate::sync::log_file_format::{
20 anchor_matches, append_record, read_header, read_payload, read_payload_from, scan_tail,
21 write_record, RecordLocation, TailScan,
22};
23use crate::sync::persistence::{
24 acquire_persistence_lock, atomic_write_with, reject_symlink, truncate_synced,
25};
26use crate::sync::snapshot::{
27 snapshot_from_payloads, validate_snapshot_contract, ReplicationSnapshot,
28};
29use std::fs::{self, File};
30use std::io::Write;
31use std::path::{Path, PathBuf};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct FileReplicationLog {
36 file_path: PathBuf,
37 state: LogState,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41struct LogState {
42 records: Vec<RecordLocation>,
43 sequence_indices: Vec<(u64, usize)>,
44 scanned_bytes: u64,
45 record_count: usize,
46 line_count: usize,
47 chain_head: String,
48}
49
50impl FileReplicationLog {
51 pub fn new(storage_path: impl AsRef<Path>, relative_path: &str) -> SyncResult<Self> {
53 let file_path = validated_path(storage_path.as_ref(), relative_path)?;
54 if let Some(parent) = file_path.parent() {
55 fs::create_dir_all(parent).map_err(replication_error)?;
56 }
57 let _process_lock = acquire_persistence_lock(&file_path)?;
58 if !file_path.exists() {
59 create_empty_log(&file_path)?;
60 }
61 let state = load_state(&file_path)?;
62 Ok(Self { file_path, state })
63 }
64
65 pub fn file_path(&self) -> &Path {
67 &self.file_path
68 }
69
70 pub fn reload(&mut self) -> SyncResult<()> {
72 let _process_lock = acquire_persistence_lock(&self.file_path)?;
73 self.state = load_state(&self.file_path)?;
74 Ok(())
75 }
76
77 pub fn events_page(
79 &self,
80 index: usize,
81 max_records: usize,
82 max_bytes: usize,
83 ) -> SyncResult<Vec<Vec<u8>>> {
84 validate_log_index(index, self.state.records.len())?;
85 validate_page_limits(max_records, max_bytes)?;
86 read_page(
87 &self.file_path,
88 &self.state.records[index..],
89 max_records,
90 max_bytes,
91 )
92 }
93
94 fn refresh_unlocked(&mut self) -> SyncResult<()> {
95 let header = read_header(&self.file_path)?;
96 if header.incomplete || header.file_bytes < self.state.scanned_bytes {
97 self.state = load_state(&self.file_path)?;
98 return Ok(());
99 }
100 if !anchor_matches(
101 &self.file_path,
102 self.state.records.last(),
103 &self.state.chain_head,
104 )? {
105 self.state = load_state(&self.file_path)?;
106 return Ok(());
107 }
108 if header.file_bytes == self.state.scanned_bytes {
109 return Ok(());
110 }
111 let tail = scan_tail(
112 &self.file_path,
113 self.state.scanned_bytes,
114 self.state.record_count,
115 self.state.line_count,
116 &self.state.chain_head,
117 &self.state.records,
118 &self.state.sequence_indices,
119 )?;
120 apply_tail(&mut self.state, tail, &self.file_path)
121 }
122
123 fn append_record(&mut self, payload: Vec<u8>, sequence: u64) -> SyncResult<usize> {
124 let _process_lock = acquire_persistence_lock(&self.file_path)?;
125 self.refresh_unlocked()?;
126 validate_record_size(&payload)?;
127 validate_record_count(self.state.record_count.saturating_add(1))?;
128 if sequence > 0 {
129 if let Some(index) = find_sequence(&self.state.sequence_indices, sequence) {
130 return if read_payload(&self.file_path, &self.state.records[index])? == payload {
131 Ok(index + 1)
132 } else {
133 Err(SyncError::SequenceConflict(sequence))
134 };
135 }
136 }
137 let previous_hash = self.state.chain_head.clone();
138 let record_hash = replication_record_hash(&previous_hash, sequence, &payload);
139 let (location, next_offset) = append_record(
140 &self.file_path,
141 self.state.scanned_bytes,
142 sequence,
143 &payload,
144 &previous_hash,
145 &record_hash,
146 )?;
147 let index = self.state.records.len();
148 self.state.records.push(location);
149 if sequence > 0 {
150 insert_sequence(&mut self.state.sequence_indices, sequence, index);
151 }
152 self.state.scanned_bytes = next_offset;
153 self.state.record_count += 1;
154 self.state.line_count += 1;
155 self.state.chain_head = record_hash;
156 Ok(index + 1)
157 }
158}
159
160impl ReplicationLog for FileReplicationLog {
161 fn append(&mut self, record: Vec<u8>) -> SyncResult<usize> {
162 self.append_record(record, 0)
163 }
164
165 fn append_with_sequence(&mut self, record: Vec<u8>, sequence: u64) -> SyncResult<usize> {
166 self.append_record(record, sequence)
167 }
168
169 fn event_at_sequence(&self, sequence: u64) -> SyncResult<Option<Vec<u8>>> {
170 if sequence == 0 {
171 return Ok(None);
172 }
173 find_sequence(&self.state.sequence_indices, sequence)
174 .map(|index| read_payload(&self.file_path, &self.state.records[index]))
175 .transpose()
176 }
177
178 fn events_since(&self, index: usize) -> SyncResult<Vec<Vec<u8>>> {
179 validate_log_index(index, self.state.records.len())?;
180 let records = &self.state.records[index..];
181 if records.len() > MAX_REPLICATION_PAGE_RECORDS
182 || aggregate_payload_bytes(records)? > MAX_REPLICATION_PAGE_BYTES
183 {
184 return Err(replication_message(
185 "complete replication read exceeds page limits; use events_page",
186 ));
187 }
188 read_page(
189 &self.file_path,
190 records,
191 MAX_REPLICATION_PAGE_RECORDS,
192 MAX_REPLICATION_PAGE_BYTES,
193 )
194 }
195
196 fn events_page(
197 &self,
198 index: usize,
199 max_records: usize,
200 max_bytes: usize,
201 ) -> SyncResult<Vec<Vec<u8>>> {
202 Self::events_page(self, index, max_records, max_bytes)
203 }
204
205 fn last_index(&self) -> SyncResult<usize> {
206 Ok(self.state.records.len())
207 }
208
209 fn len(&self) -> SyncResult<usize> {
210 Ok(self.state.records.len())
211 }
212
213 fn is_empty(&self) -> SyncResult<bool> {
214 Ok(self.state.records.is_empty())
215 }
216
217 fn create_snapshot(&self) -> SyncResult<ReplicationSnapshot> {
218 let mut file = open_log(&self.file_path)?;
219 snapshot_from_payloads(self.state.records.iter().map(|location| {
220 read_payload_from(&mut file, location).map(|payload| (location.sequence, payload))
221 }))
222 }
223
224 fn restore_snapshot(&mut self, snapshot: &ReplicationSnapshot) -> SyncResult<()> {
225 let _process_lock = acquire_persistence_lock(&self.file_path)?;
226 validate_snapshot_contract(snapshot)?;
227 write_snapshot(&self.file_path, snapshot)?;
228 self.state = load_state(&self.file_path)?;
229 Ok(())
230 }
231}
232
233fn load_state(path: &Path) -> SyncResult<LogState> {
234 let header = read_header(path)?;
235 if header.incomplete {
236 create_empty_log(path)?;
237 return Ok(empty_state());
238 }
239 let tail = scan_tail(path, header.body_offset, 0, 0, "", &[], &[])?;
240 let mut state = empty_state();
241 apply_tail(&mut state, tail, path)?;
242 Ok(state)
243}
244
245fn apply_tail(state: &mut LogState, tail: TailScan, path: &Path) -> SyncResult<()> {
246 let start = state.records.len();
247 for (offset, location) in tail.locations.iter().enumerate() {
248 if location.sequence > 0 {
249 insert_sequence(
250 &mut state.sequence_indices,
251 location.sequence,
252 start + offset,
253 );
254 }
255 }
256 state.records.extend(tail.locations);
257 state.scanned_bytes = tail.scanned_bytes;
258 state.record_count = tail.record_count;
259 state.line_count = tail.line_count;
260 state.chain_head = tail.chain_head;
261 if tail.recovered_tail {
262 truncate_synced(path, state.scanned_bytes)?;
263 }
264 Ok(())
265}
266
267fn read_page(
268 path: &Path,
269 records: &[RecordLocation],
270 max_records: usize,
271 max_bytes: usize,
272) -> SyncResult<Vec<Vec<u8>>> {
273 let mut file = open_log(path)?;
274 let mut page = Vec::with_capacity(records.len().min(max_records));
275 let mut bytes = 0usize;
276 for location in records.iter().take(max_records) {
277 let next = bytes
278 .checked_add(location.payload_bytes as usize)
279 .ok_or_else(|| replication_message("replication page overflow"))?;
280 if next > max_bytes {
281 if page.is_empty() {
282 return Err(replication_message("replication page byte limit too small"));
283 }
284 break;
285 }
286 page.push(read_payload_from(&mut file, location)?);
287 bytes = next;
288 }
289 Ok(page)
290}
291
292fn aggregate_payload_bytes(records: &[RecordLocation]) -> SyncResult<usize> {
293 records.iter().try_fold(0usize, |total, location| {
294 total
295 .checked_add(location.payload_bytes as usize)
296 .ok_or_else(|| replication_message("replication payload byte count overflow"))
297 })
298}
299
300fn write_snapshot(path: &Path, snapshot: &ReplicationSnapshot) -> SyncResult<()> {
301 atomic_write_with(path, |file| {
302 write_header(file)?;
303 let mut offset = (REPLICATION_LOG_FORMAT_V1.len() + 1) as u64;
304 let mut previous_hash = String::new();
305 for record in &snapshot.records {
306 let hash = replication_record_hash(&previous_hash, record.sequence, &record.payload);
307 let (_, written_bytes) = write_record(
308 file,
309 offset,
310 record.sequence,
311 &record.payload,
312 &previous_hash,
313 &hash,
314 )?;
315 offset = offset
316 .checked_add(written_bytes)
317 .filter(|bytes| *bytes <= MAX_REPLICATION_LOG_BYTES)
318 .ok_or_else(|| replication_message("replication log exceeds configured limit"))?;
319 previous_hash = hash;
320 }
321 Ok(())
322 })
323}
324
325fn create_empty_log(path: &Path) -> SyncResult<()> {
326 atomic_write_with(path, write_header)
327}
328
329fn write_header(file: &mut File) -> SyncResult<()> {
330 file.write_all(REPLICATION_LOG_FORMAT_V1.as_bytes())
331 .and_then(|()| file.write_all(b"\n"))
332 .map_err(replication_error)
333}
334
335fn empty_state() -> LogState {
336 LogState {
337 records: Vec::new(),
338 sequence_indices: Vec::new(),
339 scanned_bytes: (REPLICATION_LOG_FORMAT_V1.len() + 1) as u64,
340 record_count: 0,
341 line_count: 0,
342 chain_head: String::new(),
343 }
344}
345
346fn find_sequence(index: &[(u64, usize)], sequence: u64) -> Option<usize> {
347 index
348 .binary_search_by_key(&sequence, |(key, _)| *key)
349 .ok()
350 .map(|position| index[position].1)
351}
352
353fn insert_sequence(index: &mut Vec<(u64, usize)>, sequence: u64, record_index: usize) {
354 match index.binary_search_by_key(&sequence, |(key, _)| *key) {
355 Ok(position) => index[position] = (sequence, record_index),
356 Err(position) => index.insert(position, (sequence, record_index)),
357 }
358}
359
360fn validated_path(storage_root: &Path, relative_path: &str) -> SyncResult<PathBuf> {
361 let relative = PathBuf::from(relative_path);
362 if relative.as_os_str().is_empty()
363 || relative.is_absolute()
364 || relative
365 .components()
366 .any(|component| matches!(component, std::path::Component::ParentDir))
367 {
368 return Err(replication_message("invalid replication log path"));
369 }
370 Ok(storage_root.join(relative))
371}
372
373fn open_log(path: &Path) -> SyncResult<File> {
374 reject_symlink(path)?;
375 File::open(path).map_err(replication_error)
376}
377
378fn replication_message(message: &str) -> SyncError {
379 SyncError::ReplicationFailed(message.to_string())
380}
381
382fn replication_error(error: std::io::Error) -> SyncError {
383 SyncError::ReplicationFailed(error.to_string())
384}