1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::sync::{Mutex, RwLock};
13use tokio::task::JoinHandle;
14use tracing::{debug, error, info, warn};
15
16use super::backend::StateBackend;
17use crate::error::{StateError, StateResult};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CheckpointMetadata {
25 pub checkpoint_id: String,
27 pub created_at: DateTime<Utc>,
29 pub entry_count: usize,
31 pub size_bytes: u64,
33 pub checksum: String,
35 pub validated: bool,
37 pub version: u32,
39}
40
41impl CheckpointMetadata {
42 pub fn new(checkpoint_id: String, entry_count: usize, size_bytes: u64, checksum: String) -> Self {
44 Self {
45 checkpoint_id,
46 created_at: Utc::now(),
47 entry_count,
48 size_bytes,
49 checksum,
50 validated: false,
51 version: 1,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Checkpoint {
59 pub metadata: CheckpointMetadata,
61 pub data: Vec<(Vec<u8>, Vec<u8>)>,
63}
64
65impl Checkpoint {
66 pub fn new(checkpoint_id: String, data: Vec<(Vec<u8>, Vec<u8>)>) -> Self {
68 let size_bytes = data
69 .iter()
70 .map(|(k, v)| k.len() + v.len())
71 .sum::<usize>() as u64;
72
73 let checksum = Self::calculate_checksum(&data);
74
75 Self {
76 metadata: CheckpointMetadata::new(
77 checkpoint_id,
78 data.len(),
79 size_bytes,
80 checksum,
81 ),
82 data,
83 }
84 }
85
86 fn calculate_checksum(data: &[(Vec<u8>, Vec<u8>)]) -> String {
88 use std::collections::hash_map::DefaultHasher;
89 use std::hash::{Hash, Hasher};
90
91 let mut hasher = DefaultHasher::new();
92 for (key, value) in data {
93 key.hash(&mut hasher);
94 value.hash(&mut hasher);
95 }
96
97 format!("{:x}", hasher.finish())
98 }
99
100 pub fn validate(&mut self) -> StateResult<()> {
102 let calculated_checksum = Self::calculate_checksum(&self.data);
103
104 if calculated_checksum != self.metadata.checksum {
105 return Err(StateError::CheckpointFailed {
106 checkpoint_id: self.metadata.checkpoint_id.clone(),
107 reason: format!(
108 "Checksum mismatch: expected {}, got {}",
109 self.metadata.checksum, calculated_checksum
110 ),
111 });
112 }
113
114 self.metadata.validated = true;
115 Ok(())
116 }
117
118 pub async fn save<P: AsRef<Path>>(&self, path: P) -> StateResult<()> {
120 let path = path.as_ref();
121 info!("Saving checkpoint {} to {:?}", self.metadata.checkpoint_id, path);
122
123 if let Some(parent) = path.parent() {
125 tokio::fs::create_dir_all(parent).await.map_err(|e| {
126 StateError::CheckpointFailed {
127 checkpoint_id: self.metadata.checkpoint_id.clone(),
128 reason: format!("Failed to create directory: {}", e),
129 }
130 })?;
131 }
132
133 let serialized = bincode::serialize(self).map_err(|e| StateError::SerializationFailed {
135 key: self.metadata.checkpoint_id.clone(),
136 reason: e.to_string(),
137 })?;
138
139 tokio::fs::write(path, &serialized).await.map_err(|e| {
141 StateError::CheckpointFailed {
142 checkpoint_id: self.metadata.checkpoint_id.clone(),
143 reason: format!("Failed to write file: {}", e),
144 }
145 })?;
146
147 info!(
148 "Checkpoint saved: {} entries, {} bytes",
149 self.metadata.entry_count, self.metadata.size_bytes
150 );
151
152 Ok(())
153 }
154
155 pub async fn load<P: AsRef<Path>>(path: P) -> StateResult<Self> {
157 let path = path.as_ref();
158 info!("Loading checkpoint from {:?}", path);
159
160 let data = tokio::fs::read(path).await.map_err(|e| {
162 StateError::RestoreFailed {
163 checkpoint_id: path.to_string_lossy().to_string(),
164 reason: format!("Failed to read file: {}", e),
165 }
166 })?;
167
168 let mut checkpoint: Checkpoint =
170 bincode::deserialize(&data).map_err(|e| StateError::DeserializationFailed {
171 key: path.to_string_lossy().to_string(),
172 reason: e.to_string(),
173 })?;
174
175 checkpoint.validate()?;
177
178 info!(
179 "Checkpoint loaded: {} entries, {} bytes",
180 checkpoint.metadata.entry_count, checkpoint.metadata.size_bytes
181 );
182
183 Ok(checkpoint)
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct CheckpointOptions {
190 pub checkpoint_dir: PathBuf,
192 pub max_checkpoints: usize,
194 pub compress: bool,
196 pub min_interval: Duration,
198}
199
200impl Default for CheckpointOptions {
201 fn default() -> Self {
202 Self {
203 checkpoint_dir: PathBuf::from("/tmp/checkpoints"),
204 max_checkpoints: 5,
205 compress: true,
206 min_interval: Duration::from_secs(60),
207 }
208 }
209}
210
211#[derive(Debug, Clone, Default)]
213pub struct CheckpointStats {
214 pub checkpoints_created: u64,
216 pub checkpoint_failures: u64,
218 pub restores: u64,
220 pub restore_failures: u64,
222 pub last_checkpoint_time: Option<DateTime<Utc>>,
224 pub last_checkpoint_duration_ms: Option<u64>,
226 pub total_bytes_checkpointed: u64,
228}
229
230pub struct CheckpointCoordinator<B: StateBackend> {
280 backend: Arc<B>,
282 interval: Duration,
284 options: CheckpointOptions,
286 stats: Arc<RwLock<CheckpointStats>>,
288 task_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
290 shutdown_tx: Arc<Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
292}
293
294impl<B: StateBackend + 'static> CheckpointCoordinator<B> {
295 pub fn new(backend: Arc<B>, interval: Duration, options: CheckpointOptions) -> Self {
307 Self {
308 backend,
309 interval,
310 options,
311 stats: Arc::new(RwLock::new(CheckpointStats::default())),
312 task_handle: Arc::new(Mutex::new(None)),
313 shutdown_tx: Arc::new(Mutex::new(None)),
314 }
315 }
316
317 pub async fn start(&self) -> StateResult<()> {
326 let mut handle_guard = self.task_handle.lock().await;
327
328 if handle_guard.is_some() {
329 warn!("Checkpoint coordinator already started");
330 return Ok(());
331 }
332
333 info!("Starting checkpoint coordinator with interval {:?}", self.interval);
334
335 let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
336 *self.shutdown_tx.lock().await = Some(shutdown_tx);
337
338 let backend = Arc::clone(&self.backend);
339 let options = self.options.clone();
340 let stats = Arc::clone(&self.stats);
341 let interval = self.interval;
342
343 let handle = tokio::spawn(async move {
344 let mut interval_timer = tokio::time::interval(interval);
345 let mut last_checkpoint = Utc::now();
346
347 loop {
348 tokio::select! {
349 _ = interval_timer.tick() => {
350 let elapsed = Utc::now().signed_duration_since(last_checkpoint);
352 if elapsed.num_milliseconds() as u64 >= options.min_interval.as_millis() as u64 {
353 if let Err(e) = Self::create_checkpoint_internal(
354 &backend,
355 &options,
356 &stats,
357 ).await {
358 error!("Checkpoint failed: {}", e);
359 let mut stats_guard = stats.write().await;
360 stats_guard.checkpoint_failures += 1;
361 } else {
362 last_checkpoint = Utc::now();
363 }
364 }
365 }
366 _ = &mut shutdown_rx => {
367 info!("Checkpoint coordinator shutting down");
368 if let Err(e) = Self::create_checkpoint_internal(
370 &backend,
371 &options,
372 &stats,
373 ).await {
374 error!("Final checkpoint failed: {}", e);
375 }
376 break;
377 }
378 }
379 }
380 });
381
382 *handle_guard = Some(handle);
383 Ok(())
384 }
385
386 pub async fn create_checkpoint(&self) -> StateResult<String> {
393 Self::create_checkpoint_internal(&self.backend, &self.options, &self.stats).await
394 }
395
396 async fn create_checkpoint_internal(
398 backend: &Arc<B>,
399 options: &CheckpointOptions,
400 stats: &Arc<RwLock<CheckpointStats>>,
401 ) -> StateResult<String> {
402 let start_time = Utc::now();
403 let checkpoint_id = format!("checkpoint_{}", start_time.timestamp_millis());
404
405 debug!("Creating checkpoint {}", checkpoint_id);
406
407 let data = Self::export_state(backend).await?;
409
410 let checkpoint = Checkpoint::new(checkpoint_id.clone(), data);
412
413 let checkpoint_path = options
415 .checkpoint_dir
416 .join(format!("{}.ckpt", checkpoint_id));
417
418 checkpoint.save(&checkpoint_path).await?;
419
420 let duration = Utc::now().signed_duration_since(start_time);
422 let mut stats_guard = stats.write().await;
423 stats_guard.checkpoints_created += 1;
424 stats_guard.last_checkpoint_time = Some(start_time);
425 stats_guard.last_checkpoint_duration_ms = Some(duration.num_milliseconds() as u64);
426 stats_guard.total_bytes_checkpointed += checkpoint.metadata.size_bytes;
427 drop(stats_guard);
428
429 Self::cleanup_old_checkpoints(options).await?;
431
432 info!(
433 "Checkpoint {} created in {}ms",
434 checkpoint_id,
435 duration.num_milliseconds()
436 );
437
438 Ok(checkpoint_id)
439 }
440
441 async fn export_state(backend: &Arc<B>) -> StateResult<Vec<(Vec<u8>, Vec<u8>)>> {
443 let keys = backend.list_keys(b"").await?;
444 let mut data = Vec::with_capacity(keys.len());
445
446 for key in keys {
447 if let Some(value) = backend.get(&key).await? {
448 data.push((key, value));
449 }
450 }
451
452 Ok(data)
453 }
454
455 async fn cleanup_old_checkpoints(options: &CheckpointOptions) -> StateResult<()> {
457 let mut entries = tokio::fs::read_dir(&options.checkpoint_dir)
458 .await
459 .map_err(|e| StateError::StorageError {
460 backend_type: "checkpoint".to_string(),
461 details: format!("Failed to read checkpoint directory: {}", e),
462 })?;
463
464 let mut checkpoints = Vec::new();
465
466 while let Some(entry) = entries.next_entry().await.map_err(|e| {
467 StateError::StorageError {
468 backend_type: "checkpoint".to_string(),
469 details: format!("Failed to read directory entry: {}", e),
470 }
471 })? {
472 let path = entry.path();
473 if path.extension().and_then(|s| s.to_str()) == Some("ckpt") {
474 if let Ok(metadata) = entry.metadata().await {
475 if let Ok(modified) = metadata.modified() {
476 checkpoints.push((path, modified));
477 }
478 }
479 }
480 }
481
482 checkpoints.sort_by(|a, b| b.1.cmp(&a.1));
484
485 for (path, _) in checkpoints.into_iter().skip(options.max_checkpoints) {
487 debug!("Deleting old checkpoint: {:?}", path);
488 if let Err(e) = tokio::fs::remove_file(&path).await {
489 warn!("Failed to delete checkpoint {:?}: {}", path, e);
490 }
491 }
492
493 Ok(())
494 }
495
496 pub async fn restore_latest(&self) -> StateResult<String> {
503 info!("Restoring from latest checkpoint");
504
505 let checkpoint_path = self.find_latest_checkpoint().await?;
507
508 let checkpoint = Checkpoint::load(&checkpoint_path).await?;
510
511 self.restore_checkpoint(&checkpoint).await?;
513
514 let mut stats = self.stats.write().await;
515 stats.restores += 1;
516
517 Ok(checkpoint.metadata.checkpoint_id)
518 }
519
520 async fn find_latest_checkpoint(&self) -> StateResult<PathBuf> {
522 let mut entries = tokio::fs::read_dir(&self.options.checkpoint_dir)
523 .await
524 .map_err(|e| StateError::RestoreFailed {
525 checkpoint_id: "unknown".to_string(),
526 reason: format!("Failed to read checkpoint directory: {}", e),
527 })?;
528
529 let mut latest: Option<(PathBuf, std::time::SystemTime)> = None;
530
531 while let Some(entry) = entries.next_entry().await.map_err(|e| {
532 StateError::RestoreFailed {
533 checkpoint_id: "unknown".to_string(),
534 reason: format!("Failed to read directory entry: {}", e),
535 }
536 })? {
537 let path = entry.path();
538 if path.extension().and_then(|s| s.to_str()) == Some("ckpt") {
539 if let Ok(metadata) = entry.metadata().await {
540 if let Ok(modified) = metadata.modified() {
541 if latest.is_none() || modified > latest.as_ref().unwrap().1 {
542 latest = Some((path, modified));
543 }
544 }
545 }
546 }
547 }
548
549 latest.map(|(path, _)| path).ok_or_else(|| StateError::RestoreFailed {
550 checkpoint_id: "unknown".to_string(),
551 reason: "No checkpoint files found".to_string(),
552 })
553 }
554
555 async fn restore_checkpoint(&self, checkpoint: &Checkpoint) -> StateResult<()> {
557 info!(
558 "Restoring checkpoint {} with {} entries",
559 checkpoint.metadata.checkpoint_id, checkpoint.metadata.entry_count
560 );
561
562 self.backend.clear().await?;
564
565 for (key, value) in &checkpoint.data {
567 self.backend.put(key, value).await?;
568 }
569
570 info!("Checkpoint restored successfully");
571 Ok(())
572 }
573
574 pub async fn stats(&self) -> CheckpointStats {
580 self.stats.read().await.clone()
581 }
582
583 pub async fn shutdown(&self) -> StateResult<()> {
592 info!("Shutting down checkpoint coordinator");
593
594 if let Some(tx) = self.shutdown_tx.lock().await.take() {
596 let _ = tx.send(());
597 }
598
599 if let Some(handle) = self.task_handle.lock().await.take() {
601 if let Err(e) = handle.await {
602 error!("Background task panicked: {}", e);
603 }
604 }
605
606 info!("Checkpoint coordinator shutdown complete");
607 Ok(())
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::state::MemoryStateBackend;
615
616 #[tokio::test]
617 async fn test_checkpoint_creation() {
618 let backend = Arc::new(MemoryStateBackend::new(None));
619
620 backend.put(b"key1", b"value1").await.unwrap();
621 backend.put(b"key2", b"value2").await.unwrap();
622
623 let data = vec![
624 (b"key1".to_vec(), b"value1".to_vec()),
625 (b"key2".to_vec(), b"value2".to_vec()),
626 ];
627
628 let checkpoint = Checkpoint::new("test_checkpoint".to_string(), data);
629 assert_eq!(checkpoint.metadata.entry_count, 2);
630 assert!(checkpoint.metadata.size_bytes > 0);
631 }
632
633 #[tokio::test]
634 async fn test_checkpoint_validation() {
635 let data = vec![(b"key1".to_vec(), b"value1".to_vec())];
636 let mut checkpoint = Checkpoint::new("test".to_string(), data);
637
638 checkpoint.validate().unwrap();
639 assert!(checkpoint.metadata.validated);
640
641 checkpoint.data.push((b"key2".to_vec(), b"value2".to_vec()));
643
644 assert!(checkpoint.validate().is_err());
645 }
646
647 #[tokio::test]
648 async fn test_checkpoint_save_load() {
649 let temp_dir = std::env::temp_dir().join(format!("checkpoint_test_{}", uuid::Uuid::new_v4()));
650 tokio::fs::create_dir_all(&temp_dir).await.unwrap();
651
652 let data = vec![
653 (b"key1".to_vec(), b"value1".to_vec()),
654 (b"key2".to_vec(), b"value2".to_vec()),
655 ];
656
657 let checkpoint = Checkpoint::new("test".to_string(), data);
658 let checkpoint_path = temp_dir.join("test.ckpt");
659
660 checkpoint.save(&checkpoint_path).await.unwrap();
661 assert!(checkpoint_path.exists());
662
663 let loaded = Checkpoint::load(&checkpoint_path).await.unwrap();
664 assert_eq!(loaded.metadata.entry_count, 2);
665 assert!(loaded.metadata.validated);
666
667 tokio::fs::remove_dir_all(&temp_dir).await.ok();
669 }
670
671 #[tokio::test]
672 async fn test_coordinator_create_checkpoint() {
673 let temp_dir = std::env::temp_dir().join(format!("coordinator_test_{}", uuid::Uuid::new_v4()));
674
675 let backend = Arc::new(MemoryStateBackend::new(None));
676 backend.put(b"key1", b"value1").await.unwrap();
677 backend.put(b"key2", b"value2").await.unwrap();
678
679 let options = CheckpointOptions {
680 checkpoint_dir: temp_dir.clone(),
681 max_checkpoints: 3,
682 compress: false,
683 min_interval: Duration::from_secs(1),
684 };
685
686 let coordinator = CheckpointCoordinator::new(
687 backend.clone(),
688 Duration::from_secs(60),
689 options,
690 );
691
692 let checkpoint_id = coordinator.create_checkpoint().await.unwrap();
693 assert!(!checkpoint_id.is_empty());
694
695 let stats = coordinator.stats().await;
696 assert_eq!(stats.checkpoints_created, 1);
697
698 tokio::fs::remove_dir_all(&temp_dir).await.ok();
700 }
701
702 #[tokio::test]
703 async fn test_coordinator_restore() {
704 let temp_dir = std::env::temp_dir().join(format!("restore_test_{}", uuid::Uuid::new_v4()));
705
706 let backend = Arc::new(MemoryStateBackend::new(None));
707 backend.put(b"key1", b"value1").await.unwrap();
708 backend.put(b"key2", b"value2").await.unwrap();
709
710 let options = CheckpointOptions {
711 checkpoint_dir: temp_dir.clone(),
712 max_checkpoints: 3,
713 compress: false,
714 min_interval: Duration::from_secs(1),
715 };
716
717 let coordinator = CheckpointCoordinator::new(
718 backend.clone(),
719 Duration::from_secs(60),
720 options,
721 );
722
723 coordinator.create_checkpoint().await.unwrap();
725
726 backend.clear().await.unwrap();
728 assert_eq!(backend.count().await.unwrap(), 0);
729
730 coordinator.restore_latest().await.unwrap();
732 assert_eq!(backend.count().await.unwrap(), 2);
733 assert_eq!(
734 backend.get(b"key1").await.unwrap(),
735 Some(b"value1".to_vec())
736 );
737
738 tokio::fs::remove_dir_all(&temp_dir).await.ok();
740 }
741
742 #[tokio::test]
743 async fn test_checkpoint_retention() {
744 let temp_dir = std::env::temp_dir().join(format!("retention_test_{}", uuid::Uuid::new_v4()));
745
746 let backend = Arc::new(MemoryStateBackend::new(None));
747
748 let options = CheckpointOptions {
749 checkpoint_dir: temp_dir.clone(),
750 max_checkpoints: 3,
751 compress: false,
752 min_interval: Duration::from_millis(100),
753 };
754
755 let coordinator = CheckpointCoordinator::new(
756 backend.clone(),
757 Duration::from_secs(60),
758 options,
759 );
760
761 for i in 0..5 {
763 backend
764 .put(&format!("key{}", i).into_bytes(), b"value")
765 .await
766 .unwrap();
767 coordinator.create_checkpoint().await.unwrap();
768 tokio::time::sleep(Duration::from_millis(200)).await;
769 }
770
771 let mut count = 0;
773 let mut entries = tokio::fs::read_dir(&temp_dir).await.unwrap();
774 while let Some(_entry) = entries.next_entry().await.unwrap() {
775 count += 1;
776 }
777
778 assert!(count <= 3);
780
781 tokio::fs::remove_dir_all(&temp_dir).await.ok();
783 }
784}