appcore_sync/sync/
outbox.rs1use crate::sync::codec::{bytes_to_hex, hex_to_bytes};
14use crate::sync::error::{SyncError, SyncResult};
15use crate::sync::persistence::{
16 acquire_persistence_lock, atomic_write, read_bounded_text, split_format,
17};
18use crate::sync::types::SyncMessage;
19use parking_lot::Mutex;
20use std::collections::VecDeque;
21use std::path::{Path, PathBuf};
22
23pub const SYNC_OUTBOX_FORMAT_V1: &str = "# appcore-sync-outbox-v1";
25const MAX_OUTBOX_FILE_BYTES: u64 = 64 * 1024 * 1024;
26
27pub trait SyncOutbox: Send + Sync {
29 fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool>;
31 fn front(&self) -> SyncResult<Option<SyncMessage>>;
33 fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()>;
35 fn messages(&self) -> SyncResult<Vec<SyncMessage>>;
37 fn len(&self) -> SyncResult<usize>;
39 fn is_empty(&self) -> SyncResult<bool> {
41 Ok(self.len()? == 0)
42 }
43}
44
45#[derive(Debug, Default)]
46pub struct InMemorySyncOutbox {
48 messages: Mutex<VecDeque<SyncMessage>>,
49}
50
51impl InMemorySyncOutbox {
52 pub fn new() -> Self {
54 Self::default()
55 }
56}
57
58impl SyncOutbox for InMemorySyncOutbox {
59 fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool> {
60 let mut messages = self.messages.lock();
61 if messages.len() >= max_len {
62 return Ok(false);
63 }
64 messages.push_back(message);
65 Ok(true)
66 }
67
68 fn front(&self) -> SyncResult<Option<SyncMessage>> {
69 Ok(self.messages.lock().front().cloned())
70 }
71
72 fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()> {
73 let mut messages = self.messages.lock();
74 if messages.front().map(|message| message.batch_id.as_str()) != Some(batch_id) {
75 return Err(SyncError::InvalidSyncMessage(
76 "outbox acknowledgement mismatch",
77 ));
78 }
79 messages.pop_front();
80 Ok(())
81 }
82
83 fn messages(&self) -> SyncResult<Vec<SyncMessage>> {
84 Ok(self.messages.lock().iter().cloned().collect())
85 }
86
87 fn len(&self) -> SyncResult<usize> {
88 Ok(self.messages.lock().len())
89 }
90}
91
92#[derive(Debug)]
93pub struct FileSyncOutbox {
95 file_path: PathBuf,
96 messages: Mutex<VecDeque<SyncMessage>>,
97}
98
99impl FileSyncOutbox {
100 pub fn new(file_path: impl Into<PathBuf>) -> SyncResult<Self> {
102 let file_path = file_path.into();
103 if let Some(parent) = file_path.parent() {
104 std::fs::create_dir_all(parent)
105 .map_err(|error| SyncError::ReplicationFailed(error.to_string()))?;
106 }
107 let _process_lock = acquire_persistence_lock(&file_path)?;
108 let existed = file_path.exists();
109 let messages = if existed {
110 load_messages(&file_path)?
111 } else {
112 VecDeque::new()
113 };
114 let outbox = Self {
115 file_path,
116 messages: Mutex::new(messages),
117 };
118 if !existed {
119 outbox.replace(&outbox.messages.lock())?;
120 }
121 Ok(outbox)
122 }
123
124 pub fn file_path(&self) -> &Path {
126 &self.file_path
127 }
128
129 fn replace(&self, messages: &VecDeque<SyncMessage>) -> SyncResult<()> {
130 let mut encoded_file = format!("{SYNC_OUTBOX_FORMAT_V1}\n");
131 for message in messages {
132 let encoded = serde_json::to_vec(message)
133 .map_err(|error| SyncError::ReplicationFailed(error.to_string()))?;
134 encoded_file.push_str(&bytes_to_hex(&encoded));
135 encoded_file.push('\n');
136 if encoded_file.len() as u64 > MAX_OUTBOX_FILE_BYTES {
137 return Err(SyncError::ReplicationFailed(
138 "sync outbox exceeds configured limit".to_string(),
139 ));
140 }
141 }
142 atomic_write(&self.file_path, encoded_file.as_bytes())
143 }
144}
145
146impl SyncOutbox for FileSyncOutbox {
147 fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool> {
148 let mut messages = self.messages.lock();
149 let _process_lock = acquire_persistence_lock(&self.file_path)?;
150 *messages = load_messages(&self.file_path)?;
151 if messages.len() >= max_len {
152 return Ok(false);
153 }
154 let mut updated = messages.clone();
155 updated.push_back(message);
156 self.replace(&updated)?;
157 *messages = updated;
158 Ok(true)
159 }
160
161 fn front(&self) -> SyncResult<Option<SyncMessage>> {
162 let mut messages = self.messages.lock();
163 let _process_lock = acquire_persistence_lock(&self.file_path)?;
164 *messages = load_messages(&self.file_path)?;
165 Ok(messages.front().cloned())
166 }
167
168 fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()> {
169 let mut messages = self.messages.lock();
170 let _process_lock = acquire_persistence_lock(&self.file_path)?;
171 *messages = load_messages(&self.file_path)?;
172 if messages.front().map(|message| message.batch_id.as_str()) != Some(batch_id) {
173 return Err(SyncError::InvalidSyncMessage(
174 "outbox acknowledgement mismatch",
175 ));
176 }
177 let mut updated = messages.clone();
178 updated.pop_front();
179 self.replace(&updated)?;
180 *messages = updated;
181 Ok(())
182 }
183
184 fn messages(&self) -> SyncResult<Vec<SyncMessage>> {
185 let mut messages = self.messages.lock();
186 let _process_lock = acquire_persistence_lock(&self.file_path)?;
187 *messages = load_messages(&self.file_path)?;
188 Ok(messages.iter().cloned().collect())
189 }
190
191 fn len(&self) -> SyncResult<usize> {
192 let mut messages = self.messages.lock();
193 let _process_lock = acquire_persistence_lock(&self.file_path)?;
194 *messages = load_messages(&self.file_path)?;
195 Ok(messages.len())
196 }
197}
198
199fn load_messages(path: &Path) -> SyncResult<VecDeque<SyncMessage>> {
200 let contents = read_bounded_text(path, MAX_OUTBOX_FILE_BYTES)?;
201 let formatted = split_format(&contents, SYNC_OUTBOX_FORMAT_V1)?;
202 let mut messages = VecDeque::new();
203 for (line_number, line) in formatted.body.lines().enumerate() {
204 if line.is_empty() {
205 continue;
206 }
207 let bytes = hex_to_bytes(line).map_err(|_| SyncError::CorruptOutbox {
208 line: line_number + 1,
209 })?;
210 if !bytes.starts_with(b"{") {
211 return Err(SyncError::ReplicationFailed(
212 crate::sync::error::UPDATE_REQUIRED_MESSAGE.to_string(),
213 ));
214 }
215 let message = serde_json::from_slice(&bytes).map_err(|_| SyncError::CorruptOutbox {
216 line: line_number + 1,
217 })?;
218 messages.push_back(message);
219 }
220 Ok(messages)
221}