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