1use crate::sync::error::{SyncError, SyncResult};
14use crate::sync::types::SyncMessage;
15use parking_lot::Mutex;
16use std::collections::VecDeque;
17
18pub use crate::sync::outbox_journal::{FileSyncOutbox, SYNC_OUTBOX_FORMAT_V2};
19
20pub const MAX_OUTBOX_PAGE_MESSAGES: usize = 1_024;
22pub const MAX_OUTBOX_PAGE_BYTES: usize = 48 * 1024 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct SyncOutboxReceipt {
28 batch_ids: Vec<String>,
29}
30
31impl SyncOutboxReceipt {
32 pub fn new(batch_ids: Vec<String>) -> SyncResult<Self> {
34 if batch_ids.is_empty() || batch_ids.len() > MAX_OUTBOX_PAGE_MESSAGES {
35 return Err(SyncError::InvalidSyncMessage("invalid outbox receipt"));
36 }
37 for batch_id in &batch_ids {
38 validate_outbox_batch_id(batch_id)?;
39 }
40 Ok(Self { batch_ids })
41 }
42
43 pub fn batch_ids(&self) -> &[String] {
45 &self.batch_ids
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct SyncOutboxStats {
52 pub pending_messages: usize,
54 pub pending_bytes: Option<usize>,
56 pub attempted_messages: Option<usize>,
58 pub total_attempts: Option<u64>,
60 pub next_ready_at_ms: Option<u64>,
62}
63
64pub trait SyncOutbox: Send + Sync {
66 fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool>;
68 fn front(&self) -> SyncResult<Option<SyncMessage>>;
70 fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()>;
72 fn messages(&self) -> SyncResult<Vec<SyncMessage>>;
74 fn len(&self) -> SyncResult<usize>;
76 fn peek(&self, limit: usize, max_bytes: usize) -> SyncResult<Vec<SyncMessage>> {
81 validate_page_limits(limit, max_bytes)?;
82 let Some(message) = self.front()? else {
83 return Ok(Vec::new());
84 };
85 if limit == 0 || max_bytes == 0 || encoded_message_bytes(&message)? > max_bytes {
86 return Ok(Vec::new());
87 }
88 Ok(vec![message])
89 }
90 fn stats(&self) -> SyncResult<SyncOutboxStats> {
95 Ok(SyncOutboxStats {
96 pending_messages: self.len()?,
97 pending_bytes: None,
98 attempted_messages: None,
99 total_attempts: None,
100 next_ready_at_ms: None,
101 })
102 }
103 fn mark_attempt(&self, _batch_id: &str, _next_ready_at_ms: u64) -> SyncResult<u32> {
108 Err(SyncError::OutboxOperationUnsupported("mark_attempt"))
109 }
110 fn next_ready(
115 &self,
116 _now_ms: u64,
117 limit: usize,
118 max_bytes: usize,
119 ) -> SyncResult<Vec<SyncMessage>> {
120 validate_page_limits(limit, max_bytes)?;
121 self.peek(limit.min(1), max_bytes)
122 }
123 fn acknowledge_receipt(&self, receipt: &SyncOutboxReceipt) -> SyncResult<usize> {
128 let [batch_id] = receipt.batch_ids() else {
129 return Err(SyncError::OutboxOperationUnsupported(
130 "multi-message receipt",
131 ));
132 };
133 self.acknowledge_front(batch_id)?;
134 Ok(1)
135 }
136 fn is_empty(&self) -> SyncResult<bool> {
138 Ok(self.len()? == 0)
139 }
140}
141
142#[derive(Debug, Default)]
143pub struct InMemorySyncOutbox {
145 messages: Mutex<VecDeque<PendingMessage>>,
146}
147
148#[derive(Debug)]
149struct PendingMessage {
150 message: SyncMessage,
151 encoded_bytes: usize,
152 attempts: u32,
153 next_ready_at_ms: u64,
154}
155
156impl InMemorySyncOutbox {
157 pub fn new() -> Self {
159 Self::default()
160 }
161}
162
163impl SyncOutbox for InMemorySyncOutbox {
164 fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool> {
165 let encoded_bytes = encoded_message_bytes(&message)?;
166 let mut messages = self.messages.lock();
167 if messages.len() >= max_len {
168 return Ok(false);
169 }
170 messages.push_back(PendingMessage {
171 message,
172 encoded_bytes,
173 attempts: 0,
174 next_ready_at_ms: 0,
175 });
176 Ok(true)
177 }
178
179 fn front(&self) -> SyncResult<Option<SyncMessage>> {
180 Ok(self
181 .messages
182 .lock()
183 .front()
184 .map(|pending| pending.message.clone()))
185 }
186
187 fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()> {
188 let mut messages = self.messages.lock();
189 if messages
190 .front()
191 .map(|pending| pending.message.batch_id.as_str())
192 != Some(batch_id)
193 {
194 return Err(SyncError::InvalidSyncMessage(
195 "outbox acknowledgement mismatch",
196 ));
197 }
198 messages.pop_front();
199 Ok(())
200 }
201
202 fn messages(&self) -> SyncResult<Vec<SyncMessage>> {
203 Ok(self
204 .messages
205 .lock()
206 .iter()
207 .map(|pending| pending.message.clone())
208 .collect())
209 }
210
211 fn len(&self) -> SyncResult<usize> {
212 Ok(self.messages.lock().len())
213 }
214
215 fn peek(&self, limit: usize, max_bytes: usize) -> SyncResult<Vec<SyncMessage>> {
216 validate_page_limits(limit, max_bytes)?;
217 Ok(page(self.messages.lock().iter(), limit, max_bytes, None))
218 }
219
220 fn stats(&self) -> SyncResult<SyncOutboxStats> {
221 let messages = self.messages.lock();
222 let pending_bytes = messages.iter().try_fold(0usize, |total, pending| {
223 total
224 .checked_add(pending.encoded_bytes)
225 .ok_or(SyncError::InvalidSyncMessage("outbox byte overflow"))
226 })?;
227 let attempted_messages = messages
228 .iter()
229 .filter(|pending| pending.attempts > 0)
230 .count();
231 let total_attempts = messages.iter().try_fold(0u64, |total, pending| {
232 total
233 .checked_add(u64::from(pending.attempts))
234 .ok_or(SyncError::InvalidSyncMessage("outbox attempt overflow"))
235 })?;
236 Ok(SyncOutboxStats {
237 pending_messages: messages.len(),
238 pending_bytes: Some(pending_bytes),
239 attempted_messages: Some(attempted_messages),
240 total_attempts: Some(total_attempts),
241 next_ready_at_ms: messages.front().map(|pending| pending.next_ready_at_ms),
242 })
243 }
244
245 fn mark_attempt(&self, batch_id: &str, next_ready_at_ms: u64) -> SyncResult<u32> {
246 validate_outbox_batch_id(batch_id)?;
247 let mut messages = self.messages.lock();
248 let pending = messages
249 .front_mut()
250 .filter(|pending| pending.message.batch_id == batch_id)
251 .ok_or(SyncError::InvalidSyncMessage("outbox attempt mismatch"))?;
252 pending.attempts = pending
253 .attempts
254 .checked_add(1)
255 .ok_or(SyncError::InvalidSyncMessage("outbox attempt overflow"))?;
256 pending.next_ready_at_ms = next_ready_at_ms;
257 Ok(pending.attempts)
258 }
259
260 fn next_ready(
261 &self,
262 now_ms: u64,
263 limit: usize,
264 max_bytes: usize,
265 ) -> SyncResult<Vec<SyncMessage>> {
266 validate_page_limits(limit, max_bytes)?;
267 Ok(page(
268 self.messages.lock().iter(),
269 limit,
270 max_bytes,
271 Some(now_ms),
272 ))
273 }
274
275 fn acknowledge_receipt(&self, receipt: &SyncOutboxReceipt) -> SyncResult<usize> {
276 let mut messages = self.messages.lock();
277 validate_receipt_prefix(&messages, receipt)?;
278 for _ in receipt.batch_ids() {
279 messages.pop_front();
280 }
281 Ok(receipt.batch_ids().len())
282 }
283}
284
285fn page<'a>(
286 messages: impl Iterator<Item = &'a PendingMessage>,
287 limit: usize,
288 max_bytes: usize,
289 ready_at_ms: Option<u64>,
290) -> Vec<SyncMessage> {
291 let mut page = Vec::new();
292 let mut bytes = 0usize;
293 for pending in messages.take(limit) {
294 if ready_at_ms.is_some_and(|now| pending.next_ready_at_ms > now)
295 || bytes
296 .checked_add(pending.encoded_bytes)
297 .is_none_or(|total| total > max_bytes)
298 {
299 break;
300 }
301 bytes += pending.encoded_bytes;
302 page.push(pending.message.clone());
303 }
304 page
305}
306
307fn validate_receipt_prefix(
308 messages: &VecDeque<PendingMessage>,
309 receipt: &SyncOutboxReceipt,
310) -> SyncResult<()> {
311 if messages.len() < receipt.batch_ids().len()
312 || messages
313 .iter()
314 .zip(receipt.batch_ids())
315 .any(|(pending, batch_id)| pending.message.batch_id != *batch_id)
316 {
317 return Err(SyncError::InvalidSyncMessage(
318 "outbox acknowledgement mismatch",
319 ));
320 }
321 Ok(())
322}
323
324pub(crate) fn encoded_message_bytes(message: &SyncMessage) -> SyncResult<usize> {
325 serde_json::to_vec(message)
326 .map(|encoded| encoded.len())
327 .map_err(|_| SyncError::InvalidSyncMessage("outbox serialization failed"))
328}
329
330pub(crate) fn validate_page_limits(limit: usize, max_bytes: usize) -> SyncResult<()> {
331 if limit > MAX_OUTBOX_PAGE_MESSAGES || max_bytes > MAX_OUTBOX_PAGE_BYTES {
332 return Err(SyncError::InvalidSyncMessage("invalid outbox page limits"));
333 }
334 Ok(())
335}
336
337pub(crate) fn validate_outbox_batch_id(batch_id: &str) -> SyncResult<()> {
338 if batch_id.is_empty() || batch_id.len() > 1_024 || batch_id.chars().any(char::is_control) {
339 return Err(SyncError::InvalidSyncMessage("invalid outbox batch id"));
340 }
341 Ok(())
342}