use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use linkrs_core::wal::types::WalResult;
pub struct WalBuffer {
buffer: Mutex<Vec<u8>>,
pending_bytes: AtomicU64,
entry_count: AtomicU64,
flush_threshold: usize,
}
impl WalBuffer {
pub fn new(flush_threshold: usize) -> Self {
Self {
buffer: Mutex::new(Vec::with_capacity(flush_threshold)),
pending_bytes: AtomicU64::new(0),
entry_count: AtomicU64::new(0),
flush_threshold,
}
}
pub fn append(&self, data: &[u8]) {
let mut buf = self.buffer.lock().unwrap();
buf.extend_from_slice(data);
self.pending_bytes
.fetch_add(data.len() as u64, Ordering::Relaxed);
self.entry_count.fetch_add(1, Ordering::Relaxed);
}
pub fn drain(&self) -> Vec<u8> {
let mut buf = self.buffer.lock().unwrap();
let data = std::mem::take(&mut *buf);
self.pending_bytes.store(0, Ordering::Relaxed);
self.entry_count.store(0, Ordering::Relaxed);
data
}
pub fn needs_flush(&self) -> bool {
self.pending_bytes.load(Ordering::Relaxed) >= self.flush_threshold as u64
}
pub fn pending_bytes(&self) -> u64 {
self.pending_bytes.load(Ordering::Relaxed)
}
pub fn entry_count(&self) -> u64 {
self.entry_count.load(Ordering::Relaxed)
}
}
pub struct WalFlushCoordinator {
buffers: Mutex<Vec<Arc<WalBuffer>>>,
flush_signal: Arc<(Mutex<bool>, Condvar)>,
flush_thread: Mutex<Option<JoinHandle<()>>>,
flush_interval: Duration,
shutdown: AtomicBool,
last_flush_time: Mutex<Option<Instant>>,
stats: FlushStats,
}
#[derive(Debug, Default)]
pub struct FlushStats {
pub flush_count: AtomicU64,
pub bytes_flushed: AtomicU64,
pub entries_flushed: AtomicU64,
pub threshold_flushes: AtomicU64,
}
impl WalFlushCoordinator {
pub fn new(flush_interval: Duration) -> Arc<Self> {
Arc::new(Self {
buffers: Mutex::new(Vec::new()),
flush_signal: Arc::new((Mutex::new(false), Condvar::new())),
flush_thread: Mutex::new(None),
flush_interval,
shutdown: AtomicBool::new(false),
last_flush_time: Mutex::new(None),
stats: FlushStats::default(),
})
}
pub fn register_buffer(&self, buffer: Arc<WalBuffer>) {
let mut buffers = self.buffers.lock().unwrap();
buffers.push(buffer);
}
pub fn start_flush_thread<F>(self: &Arc<Self>, flush_fn: F)
where
F: Fn(&[u8]) -> WalResult<()> + Send + Sync + 'static,
{
let flush_fn = Arc::new(flush_fn);
let coordinator = Arc::clone(self);
let signal = Arc::clone(&coordinator.flush_signal);
let handle = thread::spawn(move || {
let (lock, cvar) = &*signal;
loop {
let notified = {
let mut guard = lock.lock().unwrap();
if coordinator.shutdown.load(Ordering::SeqCst) {
break;
}
let result = cvar
.wait_timeout(guard, coordinator.flush_interval)
.unwrap();
guard = result.0;
*guard = false;
!result.1.timed_out()
};
let buffers = coordinator.buffers.lock().unwrap();
let mut total_flushed = 0u64;
for buf in buffers.iter() {
if buf.pending_bytes() > 0 {
let data = buf.drain();
if !data.is_empty() {
total_flushed += data.len() as u64;
if let Err(e) = flush_fn(&data) {
log::warn!(
"WAL flush failed ({} bytes, {} entries): {}",
data.len(),
buf.entry_count(),
e
);
}
}
}
}
drop(buffers);
if total_flushed > 0 {
coordinator
.stats
.bytes_flushed
.fetch_add(total_flushed, Ordering::Relaxed);
coordinator
.stats
.flush_count
.fetch_add(1, Ordering::Relaxed);
if notified {
coordinator
.stats
.threshold_flushes
.fetch_add(1, Ordering::Relaxed);
}
if let Ok(mut guard) = coordinator.last_flush_time.lock() {
*guard = Some(Instant::now());
}
}
}
});
*self.flush_thread.lock().unwrap() = Some(handle);
}
pub fn signal_flush(&self) {
let (lock, cvar) = &*self.flush_signal;
let mut guard = lock.lock().unwrap();
*guard = true;
cvar.notify_one();
}
pub fn flush_sync<F>(&self, flush_fn: &F) -> WalResult<()>
where
F: Fn(&[u8]) -> WalResult<()>,
{
let buffers = self.buffers.lock().unwrap();
for buf in buffers.iter() {
if buf.pending_bytes() > 0 {
let data = buf.drain();
if !data.is_empty() {
flush_fn(&data)?;
}
}
}
Ok(())
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::SeqCst);
{
let (lock, cvar) = &*self.flush_signal;
let _guard = lock.lock().unwrap();
cvar.notify_one();
}
if let Some(handle) = self.flush_thread.lock().unwrap().take() {
let _ = handle.join();
}
}
pub fn stats(&self) -> &FlushStats {
&self.stats
}
}
impl Drop for WalFlushCoordinator {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
#[test]
fn test_wal_buffer_append_and_drain() {
let buf = WalBuffer::new(1024);
assert_eq!(buf.pending_bytes(), 0);
assert_eq!(buf.entry_count(), 0);
buf.append(&[1, 2, 3, 4]);
assert_eq!(buf.pending_bytes(), 4);
assert_eq!(buf.entry_count(), 1);
buf.append(&[5, 6, 7]);
assert_eq!(buf.pending_bytes(), 7);
assert_eq!(buf.entry_count(), 2);
let data = buf.drain();
assert_eq!(data, vec![1, 2, 3, 4, 5, 6, 7]);
assert_eq!(buf.pending_bytes(), 0);
assert_eq!(buf.entry_count(), 0);
}
#[test]
fn test_wal_buffer_needs_flush() {
let buf = WalBuffer::new(8);
assert!(!buf.needs_flush());
buf.append(&[0; 4]);
assert!(!buf.needs_flush());
buf.append(&[0; 5]);
assert!(buf.needs_flush());
}
#[test]
fn test_flush_coordinator_sync() {
let coordinator = WalFlushCoordinator::new(Duration::from_millis(100));
let buf = Arc::new(WalBuffer::new(1024));
coordinator.register_buffer(Arc::clone(&buf));
buf.append(&[1, 2, 3]);
let flushed = Arc::new(Mutex::new(Vec::new()));
let flushed_clone = Arc::clone(&flushed);
coordinator
.flush_sync(&move |data: &[u8]| {
flushed_clone.lock().unwrap().extend_from_slice(data);
Ok(())
})
.unwrap();
let data = flushed.lock().unwrap();
assert_eq!(&*data, &[1, 2, 3]);
}
#[test]
fn test_flush_coordinator_background_thread() {
let coordinator = WalFlushCoordinator::new(Duration::from_millis(10));
let buf = Arc::new(WalBuffer::new(1024));
coordinator.register_buffer(Arc::clone(&buf));
let flush_count = Arc::new(AtomicUsize::new(0));
let flush_count_clone = Arc::clone(&flush_count);
coordinator.start_flush_thread(move |data: &[u8]| {
flush_count_clone.fetch_add(data.len(), Ordering::SeqCst);
Ok(())
});
buf.append(&[1, 2, 3, 4, 5]);
thread::sleep(Duration::from_millis(50));
assert!(flush_count.load(Ordering::SeqCst) > 0);
coordinator.shutdown();
}
}