use crate::sync::codec::{bytes_to_hex, hex_to_bytes};
use crate::sync::error::{SyncError, SyncResult};
use crate::sync::persistence::{
acquire_persistence_lock, atomic_write, read_bounded_text, split_format,
};
use crate::sync::types::SyncMessage;
use parking_lot::Mutex;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
pub const SYNC_OUTBOX_FORMAT_V1: &str = "# appcore-sync-outbox-v1";
const MAX_OUTBOX_FILE_BYTES: u64 = 64 * 1024 * 1024;
pub trait SyncOutbox: Send + Sync {
fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool>;
fn front(&self) -> SyncResult<Option<SyncMessage>>;
fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()>;
fn messages(&self) -> SyncResult<Vec<SyncMessage>>;
fn len(&self) -> SyncResult<usize>;
fn is_empty(&self) -> SyncResult<bool> {
Ok(self.len()? == 0)
}
}
#[derive(Debug, Default)]
pub struct InMemorySyncOutbox {
messages: Mutex<VecDeque<SyncMessage>>,
}
impl InMemorySyncOutbox {
pub fn new() -> Self {
Self::default()
}
}
impl SyncOutbox for InMemorySyncOutbox {
fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool> {
let mut messages = self.messages.lock();
if messages.len() >= max_len {
return Ok(false);
}
messages.push_back(message);
Ok(true)
}
fn front(&self) -> SyncResult<Option<SyncMessage>> {
Ok(self.messages.lock().front().cloned())
}
fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()> {
let mut messages = self.messages.lock();
if messages.front().map(|message| message.batch_id.as_str()) != Some(batch_id) {
return Err(SyncError::InvalidSyncMessage(
"outbox acknowledgement mismatch",
));
}
messages.pop_front();
Ok(())
}
fn messages(&self) -> SyncResult<Vec<SyncMessage>> {
Ok(self.messages.lock().iter().cloned().collect())
}
fn len(&self) -> SyncResult<usize> {
Ok(self.messages.lock().len())
}
}
#[derive(Debug)]
pub struct FileSyncOutbox {
file_path: PathBuf,
messages: Mutex<VecDeque<SyncMessage>>,
}
impl FileSyncOutbox {
pub fn new(file_path: impl Into<PathBuf>) -> SyncResult<Self> {
let file_path = file_path.into();
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| SyncError::ReplicationFailed(error.to_string()))?;
}
let _process_lock = acquire_persistence_lock(&file_path)?;
let existed = file_path.exists();
let messages = if existed {
load_messages(&file_path)?
} else {
VecDeque::new()
};
let outbox = Self {
file_path,
messages: Mutex::new(messages),
};
if !existed {
outbox.replace(&outbox.messages.lock())?;
}
Ok(outbox)
}
pub fn file_path(&self) -> &Path {
&self.file_path
}
fn replace(&self, messages: &VecDeque<SyncMessage>) -> SyncResult<()> {
let mut encoded_file = format!("{SYNC_OUTBOX_FORMAT_V1}\n");
for message in messages {
let encoded = serde_json::to_vec(message)
.map_err(|error| SyncError::ReplicationFailed(error.to_string()))?;
encoded_file.push_str(&bytes_to_hex(&encoded));
encoded_file.push('\n');
if encoded_file.len() as u64 > MAX_OUTBOX_FILE_BYTES {
return Err(SyncError::ReplicationFailed(
"sync outbox exceeds configured limit".to_string(),
));
}
}
atomic_write(&self.file_path, encoded_file.as_bytes())
}
}
impl SyncOutbox for FileSyncOutbox {
fn try_enqueue(&self, message: SyncMessage, max_len: usize) -> SyncResult<bool> {
let mut messages = self.messages.lock();
let _process_lock = acquire_persistence_lock(&self.file_path)?;
*messages = load_messages(&self.file_path)?;
if messages.len() >= max_len {
return Ok(false);
}
let mut updated = messages.clone();
updated.push_back(message);
self.replace(&updated)?;
*messages = updated;
Ok(true)
}
fn front(&self) -> SyncResult<Option<SyncMessage>> {
let mut messages = self.messages.lock();
let _process_lock = acquire_persistence_lock(&self.file_path)?;
*messages = load_messages(&self.file_path)?;
Ok(messages.front().cloned())
}
fn acknowledge_front(&self, batch_id: &str) -> SyncResult<()> {
let mut messages = self.messages.lock();
let _process_lock = acquire_persistence_lock(&self.file_path)?;
*messages = load_messages(&self.file_path)?;
if messages.front().map(|message| message.batch_id.as_str()) != Some(batch_id) {
return Err(SyncError::InvalidSyncMessage(
"outbox acknowledgement mismatch",
));
}
let mut updated = messages.clone();
updated.pop_front();
self.replace(&updated)?;
*messages = updated;
Ok(())
}
fn messages(&self) -> SyncResult<Vec<SyncMessage>> {
let mut messages = self.messages.lock();
let _process_lock = acquire_persistence_lock(&self.file_path)?;
*messages = load_messages(&self.file_path)?;
Ok(messages.iter().cloned().collect())
}
fn len(&self) -> SyncResult<usize> {
let mut messages = self.messages.lock();
let _process_lock = acquire_persistence_lock(&self.file_path)?;
*messages = load_messages(&self.file_path)?;
Ok(messages.len())
}
}
fn load_messages(path: &Path) -> SyncResult<VecDeque<SyncMessage>> {
let contents = read_bounded_text(path, MAX_OUTBOX_FILE_BYTES)?;
let formatted = split_format(&contents, SYNC_OUTBOX_FORMAT_V1)?;
let mut messages = VecDeque::new();
for (line_number, line) in formatted.body.lines().enumerate() {
if line.is_empty() {
continue;
}
let bytes = hex_to_bytes(line).map_err(|_| SyncError::CorruptOutbox {
line: line_number + 1,
})?;
if !bytes.starts_with(b"{") {
return Err(SyncError::ReplicationFailed(
crate::sync::error::UPDATE_REQUIRED_MESSAGE.to_string(),
));
}
let message = serde_json::from_slice(&bytes).map_err(|_| SyncError::CorruptOutbox {
line: line_number + 1,
})?;
messages.push_back(message);
}
Ok(messages)
}