use std::{
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use crate::{wal::entry::Lsn, StorageError};
pub const DEFAULT_CHECKPOINT_INTERVAL_SECS: u64 = 30;
pub const DEFAULT_WAL_SIZE_THRESHOLD: u64 = 64 * 1024 * 1024;
pub const DEFAULT_KEEP_CHECKPOINTS: usize = 2;
#[derive(Debug, Clone)]
pub struct CheckpointConfig {
pub interval: Duration,
pub wal_size_threshold: u64,
pub keep_checkpoints: usize,
pub auto_checkpoint: bool,
}
impl Default for CheckpointConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(DEFAULT_CHECKPOINT_INTERVAL_SECS),
wal_size_threshold: DEFAULT_WAL_SIZE_THRESHOLD,
keep_checkpoints: DEFAULT_KEEP_CHECKPOINTS,
auto_checkpoint: true,
}
}
}
impl CheckpointConfig {
pub fn new(interval_secs: u64, wal_size_mb: u64, keep_checkpoints: usize) -> Self {
Self {
interval: Duration::from_secs(interval_secs),
wal_size_threshold: wal_size_mb * 1024 * 1024,
keep_checkpoints,
auto_checkpoint: true,
}
}
pub fn manual_only() -> Self {
Self { auto_checkpoint: false, ..Default::default() }
}
}
#[derive(Debug, Clone, Default)]
pub struct CheckpointStats {
pub checkpoints_created: u64,
pub time_triggered: u64,
pub size_triggered: u64,
pub manual_triggered: u64,
pub last_checkpoint_lsn: Option<Lsn>,
pub last_checkpoint_time_ms: Option<u64>,
pub total_bytes_checkpointed: u64,
}
#[derive(Debug)]
pub enum CheckpointMessage {
TriggerCheckpoint { manual: bool },
UpdateWalSize(u64),
Shutdown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckpointTrigger {
Time,
Size,
Manual,
}
#[derive(Debug)]
pub struct CheckpointTriggerState {
wal_size: AtomicU64,
last_checkpoint_lsn: AtomicU64,
last_checkpoint_time_ms: AtomicU64,
checkpoint_in_progress: AtomicBool,
checkpoint_requested: AtomicBool,
}
impl Default for CheckpointTriggerState {
fn default() -> Self {
Self::new()
}
}
impl CheckpointTriggerState {
pub fn new() -> Self {
Self {
wal_size: AtomicU64::new(0),
last_checkpoint_lsn: AtomicU64::new(0),
last_checkpoint_time_ms: AtomicU64::new(current_timestamp_ms()),
checkpoint_in_progress: AtomicBool::new(false),
checkpoint_requested: AtomicBool::new(false),
}
}
pub fn update_wal_size(&self, size: u64) {
self.wal_size.store(size, Ordering::Release);
}
pub fn add_wal_bytes(&self, bytes: u64) {
self.wal_size.fetch_add(bytes, Ordering::AcqRel);
}
pub fn wal_size(&self) -> u64 {
self.wal_size.load(Ordering::Acquire)
}
pub fn wal_size_since_checkpoint(&self) -> u64 {
self.wal_size.load(Ordering::Acquire)
}
pub fn record_checkpoint(&self, lsn: Lsn) {
self.last_checkpoint_lsn.store(lsn, Ordering::Release);
self.last_checkpoint_time_ms.store(current_timestamp_ms(), Ordering::Release);
self.wal_size.store(0, Ordering::Release); self.checkpoint_in_progress.store(false, Ordering::Release);
self.checkpoint_requested.store(false, Ordering::Release);
}
pub fn last_checkpoint_lsn(&self) -> Lsn {
self.last_checkpoint_lsn.load(Ordering::Acquire)
}
pub fn time_since_last_checkpoint(&self) -> Duration {
let last_time = self.last_checkpoint_time_ms.load(Ordering::Acquire);
let now = current_timestamp_ms();
Duration::from_millis(now.saturating_sub(last_time))
}
pub fn should_checkpoint(&self, config: &CheckpointConfig) -> Option<CheckpointTrigger> {
if !config.auto_checkpoint {
return None;
}
if self.checkpoint_in_progress.load(Ordering::Acquire) {
return None;
}
if self.checkpoint_requested.swap(false, Ordering::AcqRel) {
return Some(CheckpointTrigger::Manual);
}
if self.time_since_last_checkpoint() >= config.interval {
return Some(CheckpointTrigger::Time);
}
if self.wal_size_since_checkpoint() >= config.wal_size_threshold {
return Some(CheckpointTrigger::Size);
}
None
}
pub fn request_checkpoint(&self) -> bool {
if self.checkpoint_in_progress.load(Ordering::Acquire) {
return false;
}
self.checkpoint_requested.store(true, Ordering::Release);
true
}
pub fn begin_checkpoint(&self) -> bool {
self.checkpoint_in_progress
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
pub fn is_checkpoint_in_progress(&self) -> bool {
self.checkpoint_in_progress.load(Ordering::Acquire)
}
}
#[cfg(not(target_arch = "wasm32"))]
mod native {
use std::{
sync::mpsc::{self, RecvTimeoutError, SyncSender},
thread::{self, JoinHandle},
};
use parking_lot::Mutex;
use super::*;
pub type CheckpointCallback = Box<dyn Fn(CheckpointTrigger) -> Result<(), StorageError> + Send>;
pub struct CheckpointScheduler {
config: CheckpointConfig,
state: Arc<CheckpointTriggerState>,
stats: Arc<Mutex<CheckpointStats>>,
sender: Option<SyncSender<CheckpointMessage>>,
handle: Option<JoinHandle<()>>,
shutdown: AtomicBool,
}
impl std::fmt::Debug for CheckpointScheduler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheckpointScheduler")
.field("config", &self.config)
.field("state", &self.state)
.field("stats", &*self.stats.lock())
.field("shutdown", &self.shutdown.load(Ordering::Relaxed))
.finish()
}
}
impl CheckpointScheduler {
pub fn new(config: CheckpointConfig) -> Self {
Self {
config,
state: Arc::new(CheckpointTriggerState::new()),
stats: Arc::new(Mutex::new(CheckpointStats::default())),
sender: None,
handle: None,
shutdown: AtomicBool::new(false),
}
}
pub fn start(&mut self, callback: CheckpointCallback) -> Result<(), StorageError> {
if self.handle.is_some() {
return Ok(()); }
let (sender, receiver) = mpsc::sync_channel(100);
self.sender = Some(sender);
let config = self.config.clone();
let state = self.state.clone();
let stats = self.stats.clone();
let handle = thread::spawn(move || {
Self::scheduler_loop(receiver, config, state, stats, callback);
});
self.handle = Some(handle);
Ok(())
}
fn scheduler_loop(
receiver: mpsc::Receiver<CheckpointMessage>,
config: CheckpointConfig,
state: Arc<CheckpointTriggerState>,
stats: Arc<Mutex<CheckpointStats>>,
callback: CheckpointCallback,
) {
let check_interval = Duration::from_secs(1);
loop {
match receiver.recv_timeout(check_interval) {
Ok(CheckpointMessage::TriggerCheckpoint { manual }) => {
if manual {
state.request_checkpoint();
}
}
Ok(CheckpointMessage::UpdateWalSize(size)) => {
state.update_wal_size(size);
}
Ok(CheckpointMessage::Shutdown) => {
log::debug!("Checkpoint scheduler shutting down");
break;
}
Err(RecvTimeoutError::Timeout) => {
}
Err(RecvTimeoutError::Disconnected) => {
break;
}
}
if let Some(trigger) = state.should_checkpoint(&config) {
if state.begin_checkpoint() {
log::debug!("Triggering checkpoint: {:?}", trigger);
match callback(trigger) {
Ok(()) => {
let mut stats = stats.lock();
stats.checkpoints_created += 1;
match trigger {
CheckpointTrigger::Time => stats.time_triggered += 1,
CheckpointTrigger::Size => stats.size_triggered += 1,
CheckpointTrigger::Manual => stats.manual_triggered += 1,
}
}
Err(e) => {
log::error!("Checkpoint failed: {}", e);
state.checkpoint_in_progress.store(false, Ordering::Release);
}
}
}
}
}
}
pub fn state(&self) -> Arc<CheckpointTriggerState> {
self.state.clone()
}
pub fn trigger_checkpoint(&self) -> Result<(), StorageError> {
if let Some(ref sender) = self.sender {
sender.send(CheckpointMessage::TriggerCheckpoint { manual: true }).map_err(
|_| StorageError::IoError("Scheduler thread terminated".to_string()),
)?;
} else {
self.state.request_checkpoint();
}
Ok(())
}
pub fn update_wal_size(&self, size: u64) {
self.state.update_wal_size(size);
if let Some(ref sender) = self.sender {
let _ = sender.send(CheckpointMessage::UpdateWalSize(size));
}
}
pub fn add_wal_bytes(&self, bytes: u64) {
self.state.add_wal_bytes(bytes);
}
pub fn stats(&self) -> CheckpointStats {
self.stats.lock().clone()
}
pub fn record_checkpoint_complete(&self, lsn: Lsn, bytes: u64) {
self.state.record_checkpoint(lsn);
let mut stats = self.stats.lock();
stats.last_checkpoint_lsn = Some(lsn);
stats.last_checkpoint_time_ms = Some(current_timestamp_ms());
stats.total_bytes_checkpointed += bytes;
}
pub fn config(&self) -> &CheckpointConfig {
&self.config
}
pub fn shutdown(&mut self) -> Result<(), StorageError> {
if self.shutdown.swap(true, Ordering::AcqRel) {
return Ok(()); }
if let Some(ref sender) = self.sender {
let _ = sender.send(CheckpointMessage::Shutdown);
}
if let Some(handle) = self.handle.take() {
handle
.join()
.map_err(|_| StorageError::IoError("Scheduler thread panicked".to_string()))?;
}
Ok(())
}
}
impl Drop for CheckpointScheduler {
fn drop(&mut self) {
if let Err(e) = self.shutdown() {
log::error!("Error during CheckpointScheduler shutdown: {}", e);
}
}
}
}
#[cfg(target_arch = "wasm32")]
mod wasm {
use std::sync::Mutex;
use super::*;
pub struct CheckpointScheduler {
config: CheckpointConfig,
state: Arc<CheckpointTriggerState>,
stats: Arc<Mutex<CheckpointStats>>,
}
impl std::fmt::Debug for CheckpointScheduler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheckpointScheduler")
.field("config", &self.config)
.field("state", &self.state)
.finish()
}
}
impl CheckpointScheduler {
pub fn new(config: CheckpointConfig) -> Self {
Self {
config,
state: Arc::new(CheckpointTriggerState::new()),
stats: Arc::new(Mutex::new(CheckpointStats::default())),
}
}
pub fn state(&self) -> Arc<CheckpointTriggerState> {
self.state.clone()
}
pub fn poll(&self) -> Option<CheckpointTrigger> {
self.state.should_checkpoint(&self.config)
}
pub fn trigger_checkpoint(&self) -> Result<(), StorageError> {
self.state.request_checkpoint();
Ok(())
}
pub fn update_wal_size(&self, size: u64) {
self.state.update_wal_size(size);
}
pub fn add_wal_bytes(&self, bytes: u64) {
self.state.add_wal_bytes(bytes);
}
pub fn stats(&self) -> CheckpointStats {
self.stats.lock().unwrap().clone()
}
pub fn record_checkpoint_complete(&self, lsn: Lsn, bytes: u64) {
self.state.record_checkpoint(lsn);
let mut stats = self.stats.lock().unwrap();
stats.last_checkpoint_lsn = Some(lsn);
stats.last_checkpoint_time_ms = Some(current_timestamp_ms());
stats.total_bytes_checkpointed += bytes;
}
pub fn config(&self) -> &CheckpointConfig {
&self.config
}
pub fn shutdown(&mut self) -> Result<(), StorageError> {
Ok(())
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use native::{CheckpointCallback, CheckpointScheduler};
#[cfg(target_arch = "wasm32")]
pub use wasm::CheckpointScheduler;
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(test)]
mod tests {
use super::*;
#[test]
fn test_checkpoint_config_default() {
let config = CheckpointConfig::default();
assert_eq!(config.interval, Duration::from_secs(DEFAULT_CHECKPOINT_INTERVAL_SECS));
assert_eq!(config.wal_size_threshold, DEFAULT_WAL_SIZE_THRESHOLD);
assert!(config.auto_checkpoint);
}
#[test]
fn test_checkpoint_config_custom() {
let config = CheckpointConfig::new(60, 128, 5);
assert_eq!(config.interval, Duration::from_secs(60));
assert_eq!(config.wal_size_threshold, 128 * 1024 * 1024);
assert_eq!(config.keep_checkpoints, 5);
}
#[test]
fn test_trigger_state_wal_size() {
let state = CheckpointTriggerState::new();
assert_eq!(state.wal_size(), 0);
state.add_wal_bytes(1000);
assert_eq!(state.wal_size(), 1000);
state.add_wal_bytes(500);
assert_eq!(state.wal_size(), 1500);
state.update_wal_size(2000);
assert_eq!(state.wal_size(), 2000);
}
#[test]
fn test_trigger_state_checkpoint_recording() {
let state = CheckpointTriggerState::new();
state.add_wal_bytes(1000);
assert_eq!(state.wal_size(), 1000);
state.record_checkpoint(100);
assert_eq!(state.last_checkpoint_lsn(), 100);
assert_eq!(state.wal_size(), 0); }
#[test]
fn test_should_checkpoint_size_trigger() {
let state = CheckpointTriggerState::new();
let config = CheckpointConfig {
wal_size_threshold: 1000,
interval: Duration::from_secs(3600), ..Default::default()
};
state.add_wal_bytes(500);
assert!(state.should_checkpoint(&config).is_none());
state.add_wal_bytes(500);
assert_eq!(state.should_checkpoint(&config), Some(CheckpointTrigger::Size));
}
#[test]
fn test_should_checkpoint_manual_trigger() {
let state = CheckpointTriggerState::new();
let config = CheckpointConfig {
wal_size_threshold: u64::MAX,
interval: Duration::from_secs(3600),
..Default::default()
};
assert!(state.should_checkpoint(&config).is_none());
state.request_checkpoint();
assert_eq!(state.should_checkpoint(&config), Some(CheckpointTrigger::Manual));
assert!(state.should_checkpoint(&config).is_none());
}
#[test]
fn test_checkpoint_in_progress_prevents_trigger() {
let state = CheckpointTriggerState::new();
let config = CheckpointConfig {
wal_size_threshold: 1000,
interval: Duration::from_secs(3600),
..Default::default()
};
state.add_wal_bytes(2000);
assert!(state.begin_checkpoint());
assert!(state.is_checkpoint_in_progress());
assert!(state.should_checkpoint(&config).is_none());
state.record_checkpoint(100);
assert!(!state.is_checkpoint_in_progress());
}
#[test]
fn test_auto_checkpoint_disabled() {
let state = CheckpointTriggerState::new();
let config = CheckpointConfig::manual_only();
state.add_wal_bytes(u64::MAX);
assert!(state.should_checkpoint(&config).is_none());
state.request_checkpoint();
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_scheduler_create() {
let scheduler = CheckpointScheduler::new(CheckpointConfig::default());
let stats = scheduler.stats();
assert_eq!(stats.checkpoints_created, 0);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_scheduler_trigger_checkpoint() {
let scheduler = CheckpointScheduler::new(CheckpointConfig::default());
scheduler.trigger_checkpoint().unwrap();
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_scheduler_record_checkpoint() {
let scheduler = CheckpointScheduler::new(CheckpointConfig::default());
scheduler.record_checkpoint_complete(100, 5000);
let stats = scheduler.stats();
assert_eq!(stats.last_checkpoint_lsn, Some(100));
assert_eq!(stats.total_bytes_checkpointed, 5000);
}
}