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