1use crate::sync::error::{SyncError, SyncResult};
14use crate::sync::outbox::SyncOutbox;
15use crate::sync::outbox_format::{
16 append_record, corrupt_record, create_empty_journal, encoded_frame_bytes,
17 ensure_append_capacity, new_generation, outbox_full, read_header, record_hash, scan_records,
18 validate_batch_id, validate_record_data, write_frame, write_header, JournalOperation,
19 ScanResult, ACK_KIND, ACK_SPACE_RESERVE_BYTES, COMPACTION_ACK_RECORDS,
20 COMPACTION_RECLAIM_BYTES, ENQUEUE_KIND, GENERATION_BYTES, HASH_BYTES, HEADER_BYTES,
21 MAX_OUTBOX_FILE_BYTES,
22};
23use crate::sync::persistence::{acquire_persistence_lock, atomic_write_with, truncate_synced};
24use crate::sync::types::SyncMessage;
25use parking_lot::Mutex;
26use std::collections::VecDeque;
27use std::path::{Path, PathBuf};
28
29pub use crate::sync::outbox_format::SYNC_OUTBOX_FORMAT_V2;
31
32pub struct FileSyncOutbox {
34 file_path: PathBuf,
35 state: Mutex<JournalState>,
36}
37
38struct JournalState {
39 generation: [u8; GENERATION_BYTES],
40 messages: VecDeque<PendingMessage>,
41 scanned_bytes: u64,
42 record_count: u64,
43 acknowledged_records: u64,
44 live_frame_bytes: u64,
45 chain_head: [u8; HASH_BYTES],
46}
47
48struct PendingMessage {
49 message: SyncMessage,
50 frame_bytes: u64,
51}
52
53impl std::fmt::Debug for FileSyncOutbox {
54 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 formatter
56 .debug_struct("FileSyncOutbox")
57 .field("file_path", &self.file_path)
58 .field("pending_messages", &self.state.lock().messages.len())
59 .finish()
60 }
61}
62
63impl FileSyncOutbox {
64 pub fn new(file_path: impl Into<PathBuf>) -> SyncResult<Self> {
66 let file_path = file_path.into();
67 if let Some(parent) = file_path.parent() {
68 std::fs::create_dir_all(parent)
69 .map_err(|error| SyncError::ReplicationFailed(error.to_string()))?;
70 }
71 let _process_lock = acquire_persistence_lock(&file_path)?;
72 if !file_path.exists() {
73 create_empty_journal(&file_path)?;
74 }
75 let state = load_state(&file_path)?;
76 Ok(Self {
77 file_path,
78 state: Mutex::new(state),
79 })
80 }
81
82 pub fn file_path(&self) -> &Path {
84 &self.file_path
85 }
86
87 fn refresh(&self, state: &mut JournalState) -> SyncResult<()> {
88 let header = read_header(&self.file_path)?;
89 if header.generation != state.generation || header.file_bytes < state.scanned_bytes {
90 *state = load_state(&self.file_path)?;
91 return Ok(());
92 }
93 if header.file_bytes == state.scanned_bytes {
94 return Ok(());
95 }
96 let batch_ids = state
97 .messages
98 .iter()
99 .map(|pending| pending.message.batch_id.clone())
100 .collect();
101 let scan = scan_records(
102 &self.file_path,
103 state.scanned_bytes,
104 state.record_count,
105 state.chain_head,
106 batch_ids,
107 )?;
108 apply_scan(state, scan, &self.file_path)
109 }
110
111 fn compact_if_needed(&self, state: &mut JournalState) -> SyncResult<()> {
112 let reclaimable = state
113 .scanned_bytes
114 .saturating_sub(HEADER_BYTES as u64)
115 .saturating_sub(state.live_frame_bytes);
116 if reclaimable >= COMPACTION_RECLAIM_BYTES
117 || state.acknowledged_records >= COMPACTION_ACK_RECORDS
118 {
119 compact(&self.file_path, state)?;
120 }
121 Ok(())
122 }
123}
124
125impl SyncOutbox for FileSyncOutbox {
126 fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool> {
127 validate_batch_id(&message.batch_id)?;
128 let mut state = self.state.lock();
129 let _process_lock = acquire_persistence_lock(&self.file_path)?;
130 self.refresh(&mut state)?;
131 self.compact_if_needed(&mut state)?;
132 if state.messages.len() >= max_len {
133 return Ok(false);
134 }
135 let data = serde_json::to_vec(&message)
136 .map_err(|error| SyncError::ReplicationFailed(error.to_string()))?;
137 validate_record_data(&data)?;
138 let frame_bytes = encoded_frame_bytes(data.len())?;
139 if state
140 .scanned_bytes
141 .checked_add(frame_bytes)
142 .is_none_or(|size| size > MAX_OUTBOX_FILE_BYTES.saturating_sub(ACK_SPACE_RESERVE_BYTES))
143 {
144 compact(&self.file_path, &mut state)?;
145 }
146 ensure_append_capacity(state.scanned_bytes, frame_bytes, ACK_SPACE_RESERVE_BYTES)?;
147 let hash = append_record(
148 &self.file_path,
149 state.record_count,
150 state.chain_head,
151 ENQUEUE_KIND,
152 &data,
153 )?;
154 state.scanned_bytes += frame_bytes;
155 state.record_count += 1;
156 state.live_frame_bytes += frame_bytes;
157 state.chain_head = hash;
158 state.messages.push_back(PendingMessage {
159 message,
160 frame_bytes,
161 });
162 Ok(true)
163 }
164
165 fn front(&self) -> SyncResult<Option<SyncMessage>> {
166 let mut state = self.state.lock();
167 let _process_lock = acquire_persistence_lock(&self.file_path)?;
168 self.refresh(&mut state)?;
169 Ok(state
170 .messages
171 .front()
172 .map(|pending| pending.message.clone()))
173 }
174
175 fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()> {
176 validate_batch_id(batch_id)?;
177 let mut state = self.state.lock();
178 let _process_lock = acquire_persistence_lock(&self.file_path)?;
179 self.refresh(&mut state)?;
180 self.compact_if_needed(&mut state)?;
181 if state
182 .messages
183 .front()
184 .map(|pending| pending.message.batch_id.as_str())
185 != Some(batch_id)
186 {
187 return Err(SyncError::InvalidSyncMessage(
188 "outbox acknowledgement mismatch",
189 ));
190 }
191 let frame_bytes = encoded_frame_bytes(batch_id.len())?;
192 ensure_append_capacity(state.scanned_bytes, frame_bytes, 0)?;
193 let hash = append_record(
194 &self.file_path,
195 state.record_count,
196 state.chain_head,
197 ACK_KIND,
198 batch_id.as_bytes(),
199 )?;
200 let acknowledged = state
201 .messages
202 .pop_front()
203 .ok_or(SyncError::InvalidSyncMessage(
204 "outbox acknowledgement mismatch",
205 ))?;
206 state.scanned_bytes += frame_bytes;
207 state.record_count += 1;
208 state.acknowledged_records += 1;
209 state.live_frame_bytes = state
210 .live_frame_bytes
211 .saturating_sub(acknowledged.frame_bytes);
212 state.chain_head = hash;
213 Ok(())
214 }
215
216 fn messages(&self) -> SyncResult<Vec<SyncMessage>> {
217 let mut state = self.state.lock();
218 let _process_lock = acquire_persistence_lock(&self.file_path)?;
219 self.refresh(&mut state)?;
220 Ok(state
221 .messages
222 .iter()
223 .map(|pending| pending.message.clone())
224 .collect())
225 }
226
227 fn len(&self) -> SyncResult<usize> {
228 let mut state = self.state.lock();
229 let _process_lock = acquire_persistence_lock(&self.file_path)?;
230 self.refresh(&mut state)?;
231 Ok(state.messages.len())
232 }
233}
234
235fn load_state(path: &Path) -> SyncResult<JournalState> {
236 let header = read_header(path)?;
237 let scan = scan_records(
238 path,
239 HEADER_BYTES as u64,
240 0,
241 [0; HASH_BYTES],
242 VecDeque::new(),
243 )?;
244 let mut state = JournalState {
245 generation: header.generation,
246 messages: VecDeque::new(),
247 scanned_bytes: HEADER_BYTES as u64,
248 record_count: 0,
249 acknowledged_records: 0,
250 live_frame_bytes: 0,
251 chain_head: [0; HASH_BYTES],
252 };
253 apply_scan(&mut state, scan, path)?;
254 Ok(state)
255}
256
257fn apply_scan(state: &mut JournalState, scan: ScanResult, path: &Path) -> SyncResult<()> {
258 if scan.recovered_tail {
259 truncate_synced(path, scan.scanned_bytes)?;
260 }
261 for operation in scan.operations {
262 match operation {
263 JournalOperation::Enqueue {
264 message,
265 frame_bytes,
266 } => {
267 state.live_frame_bytes += frame_bytes;
268 state.messages.push_back(PendingMessage {
269 message,
270 frame_bytes,
271 });
272 }
273 JournalOperation::Acknowledge => {
274 let acknowledged = state
275 .messages
276 .pop_front()
277 .ok_or_else(|| corrupt_record(state.record_count))?;
278 state.live_frame_bytes = state
279 .live_frame_bytes
280 .saturating_sub(acknowledged.frame_bytes);
281 state.acknowledged_records += 1;
282 }
283 }
284 }
285 state.scanned_bytes = scan.scanned_bytes;
286 state.record_count = scan.record_count;
287 state.chain_head = scan.chain_head;
288 Ok(())
289}
290
291fn compact(path: &Path, state: &mut JournalState) -> SyncResult<()> {
292 let generation = new_generation();
293 let mut frame_sizes = Vec::with_capacity(state.messages.len());
294 let mut chain_head = [0; HASH_BYTES];
295 let mut record_count = 0u64;
296 let mut total_bytes = HEADER_BYTES as u64;
297 atomic_write_with(path, |file| {
298 write_header(file, generation)?;
299 for pending in &state.messages {
300 let data = serde_json::to_vec(&pending.message)
301 .map_err(|error| SyncError::ReplicationFailed(error.to_string()))?;
302 validate_record_data(&data)?;
303 record_count += 1;
304 let hash = record_hash(record_count, ENQUEUE_KIND, &data, chain_head);
305 write_frame(file, record_count, ENQUEUE_KIND, &data, chain_head, hash)?;
306 chain_head = hash;
307 let size = encoded_frame_bytes(data.len())?;
308 total_bytes = total_bytes.checked_add(size).ok_or_else(outbox_full)?;
309 if total_bytes > MAX_OUTBOX_FILE_BYTES {
310 return Err(outbox_full());
311 }
312 frame_sizes.push(size);
313 }
314 Ok(())
315 })?;
316 for (pending, frame_bytes) in state.messages.iter_mut().zip(frame_sizes) {
317 pending.frame_bytes = frame_bytes;
318 }
319 state.generation = generation;
320 state.scanned_bytes = total_bytes;
321 state.record_count = record_count;
322 state.acknowledged_records = 0;
323 state.live_frame_bytes = total_bytes.saturating_sub(HEADER_BYTES as u64);
324 state.chain_head = chain_head;
325 Ok(())
326}