use super::{
durability::{DurabilityConfig, DurabilityMode},
entry::{Lsn, WalEntry, WalOp},
writer::WalWriter,
};
use crate::StorageError;
pub const DEFAULT_CHANNEL_CAPACITY: usize = 10_000;
pub const DEFAULT_FLUSH_INTERVAL_MS: u64 = 50;
pub const DEFAULT_FLUSH_COUNT: usize = 1000;
#[derive(Debug, Clone)]
pub struct PersistenceConfig {
pub channel_capacity: usize,
pub flush_interval_ms: u64,
pub flush_count: usize,
pub durability_mode: DurabilityMode,
}
impl Default for PersistenceConfig {
fn default() -> Self {
Self {
channel_capacity: DEFAULT_CHANNEL_CAPACITY,
flush_interval_ms: DEFAULT_FLUSH_INTERVAL_MS,
flush_count: DEFAULT_FLUSH_COUNT,
durability_mode: DurabilityMode::Lazy,
}
}
}
impl PersistenceConfig {
pub fn from_durability_config(config: &DurabilityConfig) -> Self {
Self {
channel_capacity: DEFAULT_CHANNEL_CAPACITY,
flush_interval_ms: config.wal_flush_interval_ms,
flush_count: config.wal_flush_batch_size,
durability_mode: config.mode,
}
}
pub fn volatile() -> Self {
Self {
channel_capacity: DEFAULT_CHANNEL_CAPACITY,
flush_interval_ms: 0,
flush_count: usize::MAX,
durability_mode: DurabilityMode::Volatile,
}
}
pub fn lazy() -> Self {
Self {
channel_capacity: DEFAULT_CHANNEL_CAPACITY,
flush_interval_ms: DEFAULT_FLUSH_INTERVAL_MS,
flush_count: DEFAULT_FLUSH_COUNT,
durability_mode: DurabilityMode::Lazy,
}
}
pub fn durable() -> Self {
Self {
channel_capacity: DEFAULT_CHANNEL_CAPACITY,
flush_interval_ms: 0,
flush_count: 1,
durability_mode: DurabilityMode::Durable,
}
}
pub fn paranoid() -> Self {
Self {
channel_capacity: DEFAULT_CHANNEL_CAPACITY,
flush_interval_ms: 0,
flush_count: 1,
durability_mode: DurabilityMode::Paranoid,
}
}
pub fn writes_wal(&self) -> bool {
self.durability_mode.writes_wal()
}
pub fn sync_on_commit(&self) -> bool {
self.durability_mode.sync_on_commit()
}
pub fn sync_on_every_op(&self) -> bool {
self.durability_mode.sync_on_every_op()
}
}
#[derive(Debug)]
pub enum WalMessage {
Entry(WalEntry),
Flush,
FlushAndNotify(FlushNotifier),
Shutdown,
}
#[cfg(not(target_arch = "wasm32"))]
mod native {
use std::{
fs::OpenOptions,
io::{BufWriter, Seek, Write},
path::Path,
sync::{
mpsc::{self, RecvTimeoutError, SyncSender},
Arc,
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use parking_lot::Mutex;
use super::*;
#[derive(Clone)]
pub struct FlushNotifier {
completed: Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
}
impl std::fmt::Debug for FlushNotifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlushNotifier").finish()
}
}
impl FlushNotifier {
pub fn new() -> Self {
Self {
completed: Arc::new((parking_lot::Mutex::new(false), parking_lot::Condvar::new())),
}
}
pub fn notify(&self) {
let (lock, cvar) = &*self.completed;
let mut completed = lock.lock();
*completed = true;
cvar.notify_all();
}
pub fn wait(&self) {
let (lock, cvar) = &*self.completed;
let mut completed = lock.lock();
while !*completed {
cvar.wait(&mut completed);
}
}
pub fn wait_timeout(&self, timeout: Duration) -> bool {
let (lock, cvar) = &*self.completed;
let mut completed = lock.lock();
while !*completed {
let result = cvar.wait_for(&mut completed, timeout);
if result.timed_out() {
return false;
}
}
true
}
}
impl Default for FlushNotifier {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct PersistenceStats {
pub entries_sent: u64,
pub entries_written: u64,
pub batches_written: u64,
pub bytes_written: u64,
pub time_flushes: u64,
pub count_flushes: u64,
pub explicit_flushes: u64,
pub volatile_discards: u64,
pub commit_syncs: u64,
pub op_syncs: u64,
pub avg_flush_latency_us: u64,
pub max_flush_latency_us: u64,
pub pending_entries: u64,
total_flush_latency_us: u64,
flush_latency_samples: u64,
}
impl PersistenceStats {
pub fn record_flush_latency(&mut self, duration: Duration) {
let latency_us = duration.as_micros() as u64;
if latency_us > self.max_flush_latency_us {
self.max_flush_latency_us = latency_us;
}
self.total_flush_latency_us += latency_us;
self.flush_latency_samples += 1;
self.avg_flush_latency_us = self.total_flush_latency_us / self.flush_latency_samples;
}
}
pub struct PersistenceEngine {
sender: SyncSender<WalMessage>,
handle: Option<JoinHandle<()>>,
stats: Arc<Mutex<PersistenceStats>>,
next_lsn: Arc<Mutex<Lsn>>,
shutdown: Arc<Mutex<bool>>,
config: PersistenceConfig,
}
impl std::fmt::Debug for PersistenceEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PersistenceEngine")
.field("next_lsn", &*self.next_lsn.lock())
.field("shutdown", &*self.shutdown.lock())
.field("stats", &*self.stats.lock())
.finish_non_exhaustive()
}
}
impl PersistenceEngine {
pub fn new<P: AsRef<Path>>(
path: P,
config: PersistenceConfig,
) -> Result<Self, StorageError> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path.as_ref())
.map_err(|e| StorageError::IoError(e.to_string()))?;
let writer = BufWriter::new(file);
Self::with_writer(writer, config)
}
pub fn with_writer<W: Write + Seek + Send + 'static>(
writer: W,
config: PersistenceConfig,
) -> Result<Self, StorageError> {
let (sender, receiver) = mpsc::sync_channel(config.channel_capacity);
let stats = Arc::new(Mutex::new(PersistenceStats::default()));
let next_lsn = Arc::new(Mutex::new(1u64));
let shutdown = Arc::new(Mutex::new(false));
let stats_clone = stats.clone();
let flush_interval = Duration::from_millis(config.flush_interval_ms);
let flush_count = config.flush_count;
let handle = thread::spawn(move || {
Self::writer_loop(writer, receiver, stats_clone, flush_interval, flush_count);
});
Ok(Self { sender, handle: Some(handle), stats, next_lsn, shutdown, config })
}
fn writer_loop<W: Write + Seek>(
writer: W,
receiver: mpsc::Receiver<WalMessage>,
stats: Arc<Mutex<PersistenceStats>>,
flush_interval: Duration,
flush_count: usize,
) {
let mut wal_writer = match WalWriter::create(writer) {
Ok(w) => w,
Err(e) => {
log::error!("Failed to create WAL writer: {}", e);
return;
}
};
let batch_capacity = flush_count.min(DEFAULT_FLUSH_COUNT);
let mut batch: Vec<WalEntry> = Vec::with_capacity(batch_capacity);
let mut last_flush = Instant::now();
let mut pending_notifiers: Vec<FlushNotifier> = Vec::new();
loop {
let elapsed = last_flush.elapsed();
let timeout = flush_interval.saturating_sub(elapsed);
match receiver.recv_timeout(timeout) {
Ok(WalMessage::Entry(entry)) => {
batch.push(entry);
if batch.len() >= flush_count {
Self::flush_batch(
&mut wal_writer,
&mut batch,
&stats,
&mut pending_notifiers,
true,
false,
);
last_flush = Instant::now();
}
}
Ok(WalMessage::Flush) => {
Self::flush_batch(
&mut wal_writer,
&mut batch,
&stats,
&mut pending_notifiers,
false,
true,
);
last_flush = Instant::now();
}
Ok(WalMessage::FlushAndNotify(notifier)) => {
pending_notifiers.push(notifier);
Self::flush_batch(
&mut wal_writer,
&mut batch,
&stats,
&mut pending_notifiers,
false,
true,
);
last_flush = Instant::now();
}
Ok(WalMessage::Shutdown) => {
Self::flush_batch(
&mut wal_writer,
&mut batch,
&stats,
&mut pending_notifiers,
false,
false,
);
break;
}
Err(RecvTimeoutError::Timeout) => {
if !batch.is_empty() {
Self::flush_batch(
&mut wal_writer,
&mut batch,
&stats,
&mut pending_notifiers,
false,
false,
);
stats.lock().time_flushes += 1;
}
last_flush = Instant::now();
}
Err(RecvTimeoutError::Disconnected) => {
Self::flush_batch(
&mut wal_writer,
&mut batch,
&stats,
&mut pending_notifiers,
false,
false,
);
break;
}
}
}
log::debug!("WAL writer thread shutting down");
}
fn flush_batch<W: Write + Seek>(
writer: &mut WalWriter<W>,
batch: &mut Vec<WalEntry>,
stats: &Arc<Mutex<PersistenceStats>>,
pending_notifiers: &mut Vec<FlushNotifier>,
is_count_flush: bool,
is_explicit_flush: bool,
) {
if batch.is_empty() {
if is_explicit_flush {
stats.lock().explicit_flushes += 1;
}
for notifier in pending_notifiers.drain(..) {
notifier.notify();
}
return;
}
let mut bytes_written = 0u64;
let entries_count = batch.len() as u64;
let flush_start = Instant::now();
for entry in batch.drain(..) {
bytes_written += 32; if let Err(e) = writer.append(&entry) {
log::error!("Failed to write WAL entry: {}", e);
}
}
if let Err(e) = writer.flush() {
log::error!("Failed to flush WAL: {}", e);
}
let flush_duration = flush_start.elapsed();
{
let mut stats = stats.lock();
stats.entries_written += entries_count;
stats.batches_written += 1;
stats.bytes_written += bytes_written;
if is_count_flush {
stats.count_flushes += 1;
}
if is_explicit_flush {
stats.explicit_flushes += 1;
}
stats.record_flush_latency(flush_duration);
}
for notifier in pending_notifiers.drain(..) {
notifier.notify();
}
}
pub fn send(&self, op: WalOp) -> Result<Lsn, StorageError> {
let lsn = {
let mut next_lsn = self.next_lsn.lock();
let lsn = *next_lsn;
*next_lsn += 1;
lsn
};
if !self.config.writes_wal() {
self.stats.lock().volatile_discards += 1;
return Ok(lsn);
}
let timestamp_ms = current_timestamp_ms();
let entry = WalEntry::new(lsn, timestamp_ms, op);
self.sender
.send(WalMessage::Entry(entry))
.map_err(|_| StorageError::IoError("WAL writer thread terminated".to_string()))?;
self.stats.lock().entries_sent += 1;
Ok(lsn)
}
pub fn send_entry(&self, entry: WalEntry) -> Result<Lsn, StorageError> {
let lsn = entry.lsn;
{
let mut next_lsn = self.next_lsn.lock();
if entry.lsn >= *next_lsn {
*next_lsn = entry.lsn + 1;
}
}
if !self.config.writes_wal() {
self.stats.lock().volatile_discards += 1;
return Ok(lsn);
}
self.sender
.send(WalMessage::Entry(entry))
.map_err(|_| StorageError::IoError("WAL writer thread terminated".to_string()))?;
self.stats.lock().entries_sent += 1;
Ok(lsn)
}
pub fn flush(&self) -> Result<(), StorageError> {
self.sender
.send(WalMessage::Flush)
.map_err(|_| StorageError::IoError("WAL writer thread terminated".to_string()))
}
pub fn sync(&self) -> Result<(), StorageError> {
let notifier = FlushNotifier::new();
self.sender
.send(WalMessage::FlushAndNotify(notifier.clone()))
.map_err(|_| StorageError::IoError("WAL writer thread terminated".to_string()))?;
notifier.wait();
Ok(())
}
pub fn sync_timeout(&self, timeout: Duration) -> Result<bool, StorageError> {
let notifier = FlushNotifier::new();
self.sender
.send(WalMessage::FlushAndNotify(notifier.clone()))
.map_err(|_| StorageError::IoError("WAL writer thread terminated".to_string()))?;
Ok(notifier.wait_timeout(timeout))
}
pub fn next_lsn(&self) -> Lsn {
*self.next_lsn.lock()
}
pub fn stats(&self) -> PersistenceStats {
self.stats.lock().clone()
}
pub fn durability_mode(&self) -> DurabilityMode {
self.config.durability_mode
}
pub fn config(&self) -> &PersistenceConfig {
&self.config
}
pub fn shutdown(&mut self) -> Result<(), StorageError> {
{
let mut shutdown = self.shutdown.lock();
if *shutdown {
return Ok(());
}
*shutdown = true;
}
let _ = self.sender.send(WalMessage::Shutdown);
if let Some(handle) = self.handle.take() {
handle
.join()
.map_err(|_| StorageError::IoError("WAL writer thread panicked".to_string()))?;
}
Ok(())
}
pub fn is_shutdown(&self) -> bool {
*self.shutdown.lock()
}
}
impl Drop for PersistenceEngine {
fn drop(&mut self) {
if let Err(e) = self.shutdown() {
log::error!("Error during PersistenceEngine shutdown: {}", e);
}
}
}
fn current_timestamp_ms() -> u64 {
use instant::SystemTime;
SystemTime::now()
.duration_since(instant::SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
}
#[cfg(target_arch = "wasm32")]
mod wasm {
use std::sync::{Arc, Mutex};
use super::*;
#[derive(Debug, Clone, Default)]
pub struct FlushNotifier;
impl FlushNotifier {
pub fn new() -> Self {
Self
}
pub fn notify(&self) {}
pub fn wait(&self) {}
}
#[derive(Debug, Clone, Default)]
pub struct PersistenceStats {
pub entries_sent: u64,
pub entries_written: u64,
pub batches_written: u64,
pub bytes_written: u64,
pub time_flushes: u64,
pub count_flushes: u64,
pub explicit_flushes: u64,
pub volatile_discards: u64,
pub commit_syncs: u64,
pub op_syncs: u64,
pub avg_flush_latency_us: u64,
pub max_flush_latency_us: u64,
pub pending_entries: u64,
}
pub struct PersistenceEngine {
buffer: Arc<Mutex<Vec<WalEntry>>>,
stats: Arc<Mutex<PersistenceStats>>,
next_lsn: Arc<Mutex<Lsn>>,
config: PersistenceConfig,
}
impl std::fmt::Debug for PersistenceEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PersistenceEngine")
.field("next_lsn", &*self.next_lsn.lock().unwrap())
.field("buffer_len", &self.buffer.lock().unwrap().len())
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl PersistenceEngine {
pub fn new(config: PersistenceConfig) -> Result<Self, StorageError> {
Ok(Self {
buffer: Arc::new(Mutex::new(Vec::with_capacity(config.flush_count))),
stats: Arc::new(Mutex::new(PersistenceStats::default())),
next_lsn: Arc::new(Mutex::new(1u64)),
config,
})
}
pub fn send(&self, op: WalOp) -> Result<Lsn, StorageError> {
let lsn = {
let mut next_lsn = self.next_lsn.lock().unwrap();
let lsn = *next_lsn;
*next_lsn += 1;
lsn
};
if !self.config.writes_wal() {
self.stats.lock().unwrap().volatile_discards += 1;
return Ok(lsn);
}
let timestamp_ms = current_timestamp_ms();
let entry = WalEntry::new(lsn, timestamp_ms, op);
{
let mut buffer = self.buffer.lock().unwrap();
buffer.push(entry);
}
self.stats.lock().unwrap().entries_sent += 1;
Ok(lsn)
}
pub fn send_entry(&self, entry: WalEntry) -> Result<Lsn, StorageError> {
let lsn = entry.lsn;
{
let mut next_lsn = self.next_lsn.lock().unwrap();
if entry.lsn >= *next_lsn {
*next_lsn = entry.lsn + 1;
}
}
if !self.config.writes_wal() {
self.stats.lock().unwrap().volatile_discards += 1;
return Ok(lsn);
}
{
let mut buffer = self.buffer.lock().unwrap();
buffer.push(entry);
}
self.stats.lock().unwrap().entries_sent += 1;
Ok(lsn)
}
pub fn flush(&self) -> Result<(), StorageError> {
log::debug!("WASM flush requested (buffered only)");
Ok(())
}
pub fn sync(&self) -> Result<(), StorageError> {
self.flush()
}
pub fn next_lsn(&self) -> Lsn {
*self.next_lsn.lock().unwrap()
}
pub fn stats(&self) -> PersistenceStats {
self.stats.lock().unwrap().clone()
}
pub fn durability_mode(&self) -> DurabilityMode {
self.config.durability_mode
}
pub fn config(&self) -> &PersistenceConfig {
&self.config
}
pub fn buffered_entries(&self) -> Vec<WalEntry> {
self.buffer.lock().unwrap().clone()
}
pub fn clear_buffer(&self) -> Vec<WalEntry> {
let mut buffer = self.buffer.lock().unwrap();
std::mem::take(&mut *buffer)
}
pub fn shutdown(&mut self) -> Result<(), StorageError> {
Ok(())
}
pub fn is_shutdown(&self) -> bool {
false
}
}
fn current_timestamp_ms() -> u64 {
use instant::SystemTime;
SystemTime::now()
.duration_since(instant::SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use native::{FlushNotifier, PersistenceEngine, PersistenceStats};
#[cfg(target_arch = "wasm32")]
pub use wasm::{FlushNotifier, PersistenceEngine, PersistenceStats};
#[cfg(test)]
mod tests {
use std::{io::Cursor, time::Duration};
use vibesql_types::SqlValue;
use super::*;
#[test]
fn test_persistence_engine_create() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
assert_eq!(engine.next_lsn(), 1);
assert!(!engine.is_shutdown());
}
#[test]
fn test_send_entry() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
let lsn = engine
.send(WalOp::Insert { table_id: 1, row_id: 100, values: vec![SqlValue::Integer(42)] })
.unwrap();
assert_eq!(lsn, 1);
assert_eq!(engine.next_lsn(), 2);
let stats = engine.stats();
assert_eq!(stats.entries_sent, 1);
}
#[test]
fn test_send_multiple_entries() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
for i in 1..=10 {
let lsn = engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
assert_eq!(lsn, i as u64);
}
assert_eq!(engine.next_lsn(), 11);
assert_eq!(engine.stats().entries_sent, 10);
}
#[test]
fn test_sync_flushes_entries() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let mut engine =
PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
for i in 1..=5 {
engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
}
engine.sync().unwrap();
let stats = engine.stats();
assert_eq!(stats.entries_sent, 5);
assert!(stats.entries_written >= 5);
engine.shutdown().unwrap();
}
#[test]
fn test_shutdown_flushes_pending() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let mut engine =
PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
for i in 1..=3 {
engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
}
engine.shutdown().unwrap();
assert!(engine.is_shutdown());
}
#[test]
fn test_config_defaults() {
let config = PersistenceConfig::default();
assert_eq!(config.channel_capacity, DEFAULT_CHANNEL_CAPACITY);
assert_eq!(config.flush_interval_ms, DEFAULT_FLUSH_INTERVAL_MS);
assert_eq!(config.flush_count, DEFAULT_FLUSH_COUNT);
}
#[test]
fn test_flush_non_blocking() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
engine
.send(WalOp::Insert { table_id: 1, row_id: 1, values: vec![SqlValue::Integer(1)] })
.unwrap();
engine.flush().unwrap();
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_sync_with_timeout() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let mut engine =
PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
engine
.send(WalOp::Insert { table_id: 1, row_id: 1, values: vec![SqlValue::Integer(1)] })
.unwrap();
let completed = engine.sync_timeout(Duration::from_secs(5)).unwrap();
assert!(completed);
engine.shutdown().unwrap();
}
#[test]
fn test_count_based_flush() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let config = PersistenceConfig { flush_count: 5, ..Default::default() };
let mut engine = PersistenceEngine::with_writer(cursor, config).unwrap();
for i in 1..=10 {
engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
}
std::thread::sleep(Duration::from_millis(100));
let stats = engine.stats();
assert!(stats.count_flushes >= 1);
engine.shutdown().unwrap();
}
#[test]
fn test_flush_latency_tracking() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let mut engine =
PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
for i in 1..=5 {
engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
}
engine.sync().unwrap();
let stats = engine.stats();
assert!(
stats.avg_flush_latency_us > 0 || stats.max_flush_latency_us > 0,
"Flush latency should be recorded after sync"
);
assert!(
stats.max_flush_latency_us >= stats.avg_flush_latency_us,
"Max latency should be >= avg latency"
);
engine.shutdown().unwrap();
}
#[test]
fn test_flush_latency_max_tracking() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let config = PersistenceConfig { flush_count: 2, ..Default::default() };
let mut engine = PersistenceEngine::with_writer(cursor, config).unwrap();
for i in 1..=10 {
engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
}
std::thread::sleep(Duration::from_millis(100));
engine.sync().unwrap();
let stats = engine.stats();
assert!(stats.batches_written >= 1, "At least one batch should have been written");
assert!(
stats.max_flush_latency_us >= stats.avg_flush_latency_us,
"Max latency ({}) should be >= avg latency ({})",
stats.max_flush_latency_us,
stats.avg_flush_latency_us
);
engine.shutdown().unwrap();
}
#[test]
fn test_volatile_mode_discards_entries() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let config = PersistenceConfig::volatile();
let mut engine = PersistenceEngine::with_writer(cursor, config).unwrap();
for i in 1..=5 {
let lsn = engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
assert_eq!(lsn, i as u64);
}
let stats = engine.stats();
assert_eq!(stats.volatile_discards, 5);
assert_eq!(stats.entries_sent, 0);
assert_eq!(engine.durability_mode(), DurabilityMode::Volatile);
assert!(!engine.config().writes_wal());
engine.shutdown().unwrap();
}
#[test]
fn test_lazy_mode_sends_entries() {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let config = PersistenceConfig::lazy();
let mut engine = PersistenceEngine::with_writer(cursor, config).unwrap();
for i in 1..=3 {
engine
.send(WalOp::Insert {
table_id: 1,
row_id: i as u64,
values: vec![SqlValue::Integer(i)],
})
.unwrap();
}
let stats = engine.stats();
assert_eq!(stats.entries_sent, 3);
assert_eq!(stats.volatile_discards, 0);
assert_eq!(engine.durability_mode(), DurabilityMode::Lazy);
assert!(engine.config().writes_wal());
assert!(!engine.config().sync_on_commit());
engine.shutdown().unwrap();
}
#[test]
fn test_config_presets() {
let volatile = PersistenceConfig::volatile();
assert_eq!(volatile.durability_mode, DurabilityMode::Volatile);
assert!(!volatile.writes_wal());
let lazy = PersistenceConfig::lazy();
assert_eq!(lazy.durability_mode, DurabilityMode::Lazy);
assert!(lazy.writes_wal());
assert!(!lazy.sync_on_commit());
let durable = PersistenceConfig::durable();
assert_eq!(durable.durability_mode, DurabilityMode::Durable);
assert!(durable.writes_wal());
assert!(durable.sync_on_commit());
assert!(!durable.sync_on_every_op());
let paranoid = PersistenceConfig::paranoid();
assert_eq!(paranoid.durability_mode, DurabilityMode::Paranoid);
assert!(paranoid.writes_wal());
assert!(paranoid.sync_on_commit());
assert!(paranoid.sync_on_every_op());
}
#[test]
fn test_config_from_durability_config() {
use crate::wal::durability::DurabilityConfig;
let dur_config = DurabilityConfig::durable();
let config = PersistenceConfig::from_durability_config(&dur_config);
assert_eq!(config.durability_mode, DurabilityMode::Durable);
assert_eq!(config.flush_interval_ms, dur_config.wal_flush_interval_ms);
assert_eq!(config.flush_count, dur_config.wal_flush_batch_size);
}
#[test]
fn test_durability_mode_getter() {
for (mode, config) in [
(DurabilityMode::Volatile, PersistenceConfig::volatile()),
(DurabilityMode::Lazy, PersistenceConfig::lazy()),
(DurabilityMode::Durable, PersistenceConfig::durable()),
(DurabilityMode::Paranoid, PersistenceConfig::paranoid()),
] {
let buf: Vec<u8> = Vec::new();
let cursor = Cursor::new(buf);
let mut engine = PersistenceEngine::with_writer(cursor, config).unwrap();
assert_eq!(engine.durability_mode(), mode);
engine.shutdown().unwrap();
}
}
}