1use crate::error::{Result, StreamingError};
4use crate::state::operator_state::DynOperatorState;
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::sync::RwLock;
12use tokio::time::sleep;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CheckpointMetadata {
17 pub id: u64,
19
20 pub timestamp: DateTime<Utc>,
22
23 pub size_bytes: usize,
25
26 pub operator_states: HashMap<String, Vec<u8>>,
28
29 pub success: bool,
31
32 pub duration: Duration,
34}
35
36#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
38pub struct CheckpointBarrier {
39 pub id: u64,
41
42 pub timestamp: DateTime<Utc>,
44}
45
46impl CheckpointBarrier {
47 pub fn new(id: u64) -> Self {
49 Self {
50 id,
51 timestamp: Utc::now(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CheckpointConfig {
59 pub interval: Duration,
61
62 pub min_pause: Duration,
64
65 pub max_concurrent: usize,
67
68 pub unaligned: bool,
70
71 pub timeout: Duration,
73
74 pub storage_path: Option<PathBuf>,
76}
77
78impl Default for CheckpointConfig {
79 fn default() -> Self {
80 Self {
81 interval: Duration::from_secs(60),
82 min_pause: Duration::from_secs(10),
83 max_concurrent: 1,
84 unaligned: false,
85 timeout: Duration::from_secs(300),
86 storage_path: None,
87 }
88 }
89}
90
91pub trait CheckpointStorage: Send + Sync {
93 fn store(&self, checkpoint: &Checkpoint) -> Result<()>;
95
96 fn load(&self, checkpoint_id: u64) -> Result<Option<Checkpoint>>;
98
99 fn delete(&self, checkpoint_id: u64) -> Result<()>;
101
102 fn list(&self) -> Result<Vec<u64>>;
104
105 fn latest(&self) -> Result<Option<u64>>;
107}
108
109pub struct FileCheckpointStorage {
116 root: PathBuf,
117}
118
119impl FileCheckpointStorage {
120 pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
124 let root = root.into();
125 std::fs::create_dir_all(&root)?;
126 Ok(Self { root })
127 }
128
129 fn checkpoint_path(&self, checkpoint_id: u64) -> PathBuf {
131 self.root.join(format!("checkpoint-{checkpoint_id}.ckpt"))
132 }
133
134 fn parse_id(name: &Path) -> Option<u64> {
136 let name = name.file_name()?.to_str()?;
137 let rest = name.strip_prefix("checkpoint-")?;
138 let id = rest.strip_suffix(".ckpt")?;
139 id.parse::<u64>().ok()
140 }
141}
142
143impl CheckpointStorage for FileCheckpointStorage {
144 fn store(&self, checkpoint: &Checkpoint) -> Result<()> {
145 let bytes = serde_json::to_vec(checkpoint)?;
150 let final_path = self.checkpoint_path(checkpoint.id());
152 let tmp_path = final_path.with_extension("ckpt.tmp");
153 std::fs::write(&tmp_path, &bytes)?;
154 std::fs::rename(&tmp_path, &final_path)?;
155 Ok(())
156 }
157
158 fn load(&self, checkpoint_id: u64) -> Result<Option<Checkpoint>> {
159 let path = self.checkpoint_path(checkpoint_id);
160 match std::fs::read(&path) {
161 Ok(bytes) => {
162 let checkpoint: Checkpoint = serde_json::from_slice(&bytes)?;
163 Ok(Some(checkpoint))
164 }
165 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
166 Err(e) => Err(e.into()),
167 }
168 }
169
170 fn delete(&self, checkpoint_id: u64) -> Result<()> {
171 let path = self.checkpoint_path(checkpoint_id);
172 match std::fs::remove_file(&path) {
173 Ok(()) => Ok(()),
174 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
175 Err(e) => Err(e.into()),
176 }
177 }
178
179 fn list(&self) -> Result<Vec<u64>> {
180 let mut ids = Vec::new();
181 for entry in std::fs::read_dir(&self.root)? {
182 let entry = entry?;
183 if let Some(id) = Self::parse_id(&entry.path()) {
184 ids.push(id);
185 }
186 }
187 ids.sort_unstable();
188 Ok(ids)
189 }
190
191 fn latest(&self) -> Result<Option<u64>> {
192 Ok(self.list()?.into_iter().max())
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Checkpoint {
199 pub metadata: CheckpointMetadata,
201
202 pub data: Vec<u8>,
204}
205
206impl Checkpoint {
207 pub fn new(id: u64, data: Vec<u8>) -> Self {
209 let size_bytes = data.len();
210 Self {
211 metadata: CheckpointMetadata {
212 id,
213 timestamp: Utc::now(),
214 size_bytes,
215 operator_states: HashMap::new(),
216 success: true,
217 duration: Duration::ZERO,
218 },
219 data,
220 }
221 }
222
223 pub fn id(&self) -> u64 {
225 self.metadata.id
226 }
227
228 pub fn size(&self) -> usize {
230 self.metadata.size_bytes
231 }
232}
233
234pub struct CheckpointCoordinator {
236 config: CheckpointConfig,
237 next_checkpoint_id: Arc<RwLock<u64>>,
238 active_checkpoints: Arc<RwLock<HashMap<u64, CheckpointMetadata>>>,
239 completed_checkpoints: Arc<RwLock<Vec<u64>>>,
240 last_checkpoint_time: Arc<RwLock<Option<DateTime<Utc>>>>,
241 operators: Arc<RwLock<HashMap<String, Arc<dyn DynOperatorState>>>>,
243 storage: Option<Arc<dyn CheckpointStorage>>,
245}
246
247impl CheckpointCoordinator {
248 pub fn new(config: CheckpointConfig) -> Self {
254 Self {
255 config,
256 next_checkpoint_id: Arc::new(RwLock::new(0)),
257 active_checkpoints: Arc::new(RwLock::new(HashMap::new())),
258 completed_checkpoints: Arc::new(RwLock::new(Vec::new())),
259 last_checkpoint_time: Arc::new(RwLock::new(None)),
260 operators: Arc::new(RwLock::new(HashMap::new())),
261 storage: None,
262 }
263 }
264
265 pub fn with_storage(config: CheckpointConfig, storage: Arc<dyn CheckpointStorage>) -> Self {
267 let mut coordinator = Self::new(config);
268 coordinator.storage = Some(storage);
269 coordinator
270 }
271
272 pub fn from_config(config: CheckpointConfig) -> Result<Self> {
278 match &config.storage_path {
279 Some(path) => {
280 let storage = Arc::new(FileCheckpointStorage::new(path.clone())?);
281 Ok(Self::with_storage(config, storage))
282 }
283 None => Ok(Self::new(config)),
284 }
285 }
286
287 pub async fn register_operator(
293 &self,
294 name: impl Into<String>,
295 state: Arc<dyn DynOperatorState>,
296 ) {
297 self.operators.write().await.insert(name.into(), state);
298 }
299
300 pub async fn operator_count(&self) -> usize {
302 self.operators.read().await.len()
303 }
304
305 pub async fn trigger_checkpoint(&self) -> Result<u64> {
318 let now = Utc::now();
319 let last_time = *self.last_checkpoint_time.read().await;
320
321 if let Some(last) = last_time {
322 let min_pause_chrono = match chrono::Duration::from_std(self.config.min_pause) {
323 Ok(duration) => duration,
324 Err(_) => chrono::Duration::zero(),
325 };
326
327 if now - last < min_pause_chrono {
328 return Err(StreamingError::CheckpointError(
329 "Minimum pause not elapsed".to_string(),
330 ));
331 }
332 }
333
334 let active_count = self.active_checkpoints.read().await.len();
335 if active_count >= self.config.max_concurrent {
336 return Err(StreamingError::CheckpointError(
337 "Too many concurrent checkpoints".to_string(),
338 ));
339 }
340
341 let handles: Vec<(String, Arc<dyn DynOperatorState>)> = {
345 let operators = self.operators.read().await;
346 operators
347 .iter()
348 .map(|(name, state)| (name.clone(), state.clone()))
349 .collect()
350 };
351
352 let mut operator_states = HashMap::with_capacity(handles.len());
353 let mut size_bytes = 0usize;
354 for (name, state) in handles {
355 let bytes = state.snapshot_boxed().await?;
356 size_bytes = size_bytes.saturating_add(bytes.len());
357 operator_states.insert(name, bytes);
358 }
359
360 let mut next_id = self.next_checkpoint_id.write().await;
361 let checkpoint_id = *next_id;
362 *next_id += 1;
363
364 let metadata = CheckpointMetadata {
365 id: checkpoint_id,
366 timestamp: now,
367 size_bytes,
368 operator_states,
369 success: false,
370 duration: Duration::ZERO,
371 };
372
373 self.active_checkpoints
374 .write()
375 .await
376 .insert(checkpoint_id, metadata);
377
378 *self.last_checkpoint_time.write().await = Some(now);
379
380 Ok(checkpoint_id)
381 }
382
383 async fn persist_checkpoint(&self, checkpoint_id: u64) -> Result<()> {
390 let Some(storage) = self.storage.clone() else {
391 return Ok(());
392 };
393
394 let metadata = {
395 let active = self.active_checkpoints.read().await;
396 active.get(&checkpoint_id).cloned().ok_or_else(|| {
397 StreamingError::CheckpointError(format!("Checkpoint {checkpoint_id} not found"))
398 })?
399 };
400
401 let data = oxicode::encode_to_vec(&metadata.operator_states)
402 .map_err(|e| StreamingError::SerializationError(e.to_string()))?;
403 let checkpoint = Checkpoint { metadata, data };
404
405 tokio::task::spawn_blocking(move || storage.store(&checkpoint))
406 .await
407 .map_err(|e| {
408 StreamingError::CheckpointError(format!("checkpoint store task failed: {e}"))
409 })??;
410
411 Ok(())
412 }
413
414 pub async fn checkpoint(&self) -> Result<u64> {
422 let checkpoint_id = self.trigger_checkpoint().await?;
423
424 if let Err(e) = self.persist_checkpoint(checkpoint_id).await {
425 let _ = self.complete_checkpoint(checkpoint_id, false).await;
428 return Err(e);
429 }
430
431 self.complete_checkpoint(checkpoint_id, true).await?;
432 Ok(checkpoint_id)
433 }
434
435 pub async fn restore_checkpoint(&self, checkpoint_id: u64) -> Result<()> {
441 let Some(storage) = self.storage.clone() else {
442 return Err(StreamingError::CheckpointError(
443 "no checkpoint storage configured".to_string(),
444 ));
445 };
446
447 let checkpoint = tokio::task::spawn_blocking(move || storage.load(checkpoint_id))
448 .await
449 .map_err(|e| {
450 StreamingError::CheckpointError(format!("checkpoint load task failed: {e}"))
451 })??
452 .ok_or_else(|| {
453 StreamingError::CheckpointError(format!(
454 "Checkpoint {checkpoint_id} not found in storage"
455 ))
456 })?;
457
458 let handles: Vec<(String, Arc<dyn DynOperatorState>)> = {
459 let operators = self.operators.read().await;
460 operators
461 .iter()
462 .map(|(name, state)| (name.clone(), state.clone()))
463 .collect()
464 };
465
466 for (name, state) in handles {
467 if let Some(bytes) = checkpoint.metadata.operator_states.get(&name) {
468 state.restore_boxed(bytes).await?;
469 }
470 }
471
472 Ok(())
473 }
474
475 pub async fn complete_checkpoint(&self, checkpoint_id: u64, success: bool) -> Result<()> {
477 let mut active = self.active_checkpoints.write().await;
478
479 if let Some(mut metadata) = active.remove(&checkpoint_id) {
480 metadata.success = success;
481 metadata.duration = match (Utc::now() - metadata.timestamp).to_std() {
482 Ok(duration) => duration,
483 Err(_) => Duration::ZERO,
484 };
485
486 if success {
487 self.completed_checkpoints.write().await.push(checkpoint_id);
488 }
489
490 Ok(())
491 } else {
492 Err(StreamingError::CheckpointError(format!(
493 "Checkpoint {} not found",
494 checkpoint_id
495 )))
496 }
497 }
498
499 pub async fn active_count(&self) -> usize {
501 self.active_checkpoints.read().await.len()
502 }
503
504 pub async fn active_metadata(&self, checkpoint_id: u64) -> Option<CheckpointMetadata> {
507 self.active_checkpoints
508 .read()
509 .await
510 .get(&checkpoint_id)
511 .cloned()
512 }
513
514 pub async fn completed_count(&self) -> usize {
516 self.completed_checkpoints.read().await.len()
517 }
518
519 pub async fn latest_checkpoint(&self) -> Option<u64> {
521 self.completed_checkpoints.read().await.last().copied()
522 }
523
524 pub async fn clear_old_checkpoints(&self, keep_count: usize) {
526 let mut completed = self.completed_checkpoints.write().await;
527
528 if completed.len() > keep_count {
529 let to_remove = completed.len() - keep_count;
530 completed.drain(0..to_remove);
531 }
532 }
533
534 pub async fn start_periodic_checkpointing(self: Arc<Self>) {
544 let interval = self.config.interval;
545 let timeout = self.config.timeout;
546
547 tokio::spawn(async move {
548 loop {
549 sleep(interval).await;
550
551 match tokio::time::timeout(timeout, self.checkpoint()).await {
552 Ok(Ok(id)) => {
553 tracing::info!("Completed checkpoint {}", id);
554 }
555 Ok(Err(e)) => {
556 tracing::warn!("Checkpoint failed: {}", e);
557 }
558 Err(_) => {
559 tracing::error!("Checkpoint timed out after {:?}", timeout);
560 }
561 }
562 }
563 });
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use crate::state::operator_state::{BroadcastState, OperatorState};
571
572 struct FailingOperator;
574
575 impl OperatorState for FailingOperator {
576 async fn snapshot(&self) -> Result<Vec<u8>> {
577 Err(StreamingError::StateError("snapshot failed".to_string()))
578 }
579
580 async fn restore(&self, _snapshot: &[u8]) -> Result<()> {
581 Ok(())
582 }
583 }
584
585 struct FailingStorage;
587
588 impl CheckpointStorage for FailingStorage {
589 fn store(&self, _checkpoint: &Checkpoint) -> Result<()> {
590 Err(StreamingError::CheckpointError("store failed".to_string()))
591 }
592 fn load(&self, _checkpoint_id: u64) -> Result<Option<Checkpoint>> {
593 Ok(None)
594 }
595 fn delete(&self, _checkpoint_id: u64) -> Result<()> {
596 Ok(())
597 }
598 fn list(&self) -> Result<Vec<u64>> {
599 Ok(Vec::new())
600 }
601 fn latest(&self) -> Result<Option<u64>> {
602 Ok(None)
603 }
604 }
605
606 fn no_pause_config() -> CheckpointConfig {
607 CheckpointConfig {
608 min_pause: Duration::ZERO,
609 max_concurrent: 4,
610 ..Default::default()
611 }
612 }
613
614 #[tokio::test]
615 async fn test_checkpoint_captures_operator_state() {
616 let coordinator = CheckpointCoordinator::new(no_pause_config());
617
618 let state = Arc::new(BroadcastState::new());
619 state.put(vec![1], vec![42]).await;
620 state.put(vec![2], vec![43]).await;
621 coordinator
622 .register_operator("op1", state.clone() as Arc<dyn DynOperatorState>)
623 .await;
624
625 assert_eq!(coordinator.operator_count().await, 1);
626
627 let id = coordinator
628 .trigger_checkpoint()
629 .await
630 .expect("trigger should succeed");
631
632 let metadata = coordinator
633 .active_metadata(id)
634 .await
635 .expect("active metadata should exist");
636
637 assert!(metadata.operator_states.contains_key("op1"));
639 assert!(!metadata.operator_states["op1"].is_empty());
640 assert!(metadata.size_bytes > 0);
641 }
642
643 #[tokio::test]
644 async fn test_checkpoint_persist_and_restore() {
645 let dir = tempfile::tempdir().expect("temp dir");
646 let storage: Arc<dyn CheckpointStorage> =
647 Arc::new(FileCheckpointStorage::new(dir.path().to_path_buf()).expect("storage"));
648
649 let coordinator = CheckpointCoordinator::with_storage(no_pause_config(), storage.clone());
650
651 let state = Arc::new(BroadcastState::new());
652 state.put(vec![7], vec![99]).await;
653 coordinator
654 .register_operator("opA", state as Arc<dyn DynOperatorState>)
655 .await;
656
657 let id = coordinator
658 .checkpoint()
659 .await
660 .expect("durable checkpoint should succeed");
661
662 assert_eq!(coordinator.completed_count().await, 1);
664 assert_eq!(coordinator.active_count().await, 0);
665
666 let loaded = storage.load(id).expect("load").expect("checkpoint present");
668 assert_eq!(loaded.metadata.operator_states["opA"], {
669 let tmp = BroadcastState::new();
670 tmp.put(vec![7], vec![99]).await;
671 tmp.snapshot().await.expect("snapshot")
672 });
673
674 let recovered = CheckpointCoordinator::with_storage(no_pause_config(), storage.clone());
676 let restored_state = Arc::new(BroadcastState::new());
677 recovered
678 .register_operator("opA", restored_state.clone() as Arc<dyn DynOperatorState>)
679 .await;
680 recovered
681 .restore_checkpoint(id)
682 .await
683 .expect("restore should succeed");
684
685 assert_eq!(restored_state.get(&[7]).await, Some(vec![99]));
686 }
687
688 #[tokio::test]
689 async fn test_checkpoint_store_failure_reports_failure() {
690 let coordinator =
691 CheckpointCoordinator::with_storage(no_pause_config(), Arc::new(FailingStorage));
692
693 let state = Arc::new(BroadcastState::new());
694 state.put(vec![1], vec![1]).await;
695 coordinator
696 .register_operator("op1", state as Arc<dyn DynOperatorState>)
697 .await;
698
699 let result = coordinator.checkpoint().await;
700 assert!(result.is_err(), "store failure must surface as an error");
701
702 assert_eq!(coordinator.completed_count().await, 0);
704 assert_eq!(coordinator.active_count().await, 0);
705 }
706
707 #[tokio::test]
708 async fn test_checkpoint_snapshot_failure_reports_failure() {
709 let coordinator = CheckpointCoordinator::new(no_pause_config());
710 coordinator
711 .register_operator(
712 "bad",
713 Arc::new(FailingOperator) as Arc<dyn DynOperatorState>,
714 )
715 .await;
716
717 let result = coordinator.checkpoint().await;
718 assert!(result.is_err(), "snapshot failure must surface as an error");
719
720 assert_eq!(coordinator.completed_count().await, 0);
721 assert_eq!(coordinator.active_count().await, 0);
722 }
723
724 #[tokio::test]
725 async fn test_file_checkpoint_storage_roundtrip() {
726 let dir = tempfile::tempdir().expect("temp dir");
727 let storage = FileCheckpointStorage::new(dir.path().to_path_buf()).expect("storage");
728
729 assert_eq!(storage.list().expect("list"), Vec::<u64>::new());
730 assert_eq!(storage.latest().expect("latest"), None);
731 assert!(storage.load(0).expect("load missing").is_none());
732
733 let checkpoint = Checkpoint::new(3, vec![1, 2, 3, 4]);
734 storage.store(&checkpoint).expect("store");
735
736 assert_eq!(storage.list().expect("list"), vec![3]);
737 assert_eq!(storage.latest().expect("latest"), Some(3));
738
739 let loaded = storage.load(3).expect("load").expect("present");
740 assert_eq!(loaded.id(), 3);
741 assert_eq!(loaded.data, vec![1, 2, 3, 4]);
742
743 storage.delete(3).expect("delete");
744 assert!(storage.load(3).expect("load after delete").is_none());
745 assert_eq!(storage.list().expect("list"), Vec::<u64>::new());
746 }
747
748 #[tokio::test]
749 async fn test_checkpoint_creation() {
750 let data = vec![1, 2, 3, 4];
751 let checkpoint = Checkpoint::new(1, data.clone());
752
753 assert_eq!(checkpoint.id(), 1);
754 assert_eq!(checkpoint.size(), 4);
755 assert_eq!(checkpoint.data, data);
756 }
757
758 #[tokio::test]
759 async fn test_checkpoint_barrier() {
760 let barrier = CheckpointBarrier::new(1);
761 assert_eq!(barrier.id, 1);
762 }
763
764 #[tokio::test]
765 async fn test_checkpoint_coordinator() {
766 let config = CheckpointConfig {
767 min_pause: Duration::ZERO, max_concurrent: 2, ..Default::default()
770 };
771 let coordinator = CheckpointCoordinator::new(config);
772
773 let id1 = coordinator
774 .trigger_checkpoint()
775 .await
776 .expect("First checkpoint trigger should succeed");
777 assert_eq!(id1, 0);
778
779 let id2 = coordinator
780 .trigger_checkpoint()
781 .await
782 .expect("Second checkpoint trigger should succeed");
783 assert_eq!(id2, 1);
784
785 assert_eq!(coordinator.active_count().await, 2);
786
787 coordinator
788 .complete_checkpoint(id1, true)
789 .await
790 .expect("Checkpoint completion should succeed");
791 assert_eq!(coordinator.active_count().await, 1);
792 assert_eq!(coordinator.completed_count().await, 1);
793 }
794
795 #[tokio::test]
796 async fn test_checkpoint_min_pause() {
797 let config = CheckpointConfig {
798 min_pause: Duration::from_secs(60),
799 ..Default::default()
800 };
801
802 let coordinator = CheckpointCoordinator::new(config);
803
804 coordinator
805 .trigger_checkpoint()
806 .await
807 .expect("First checkpoint should trigger successfully");
808 let result = coordinator.trigger_checkpoint().await;
809
810 assert!(result.is_err());
811 }
812
813 #[tokio::test]
814 async fn test_clear_old_checkpoints() {
815 let config = CheckpointConfig {
816 min_pause: Duration::ZERO, ..Default::default()
818 };
819 let coordinator = CheckpointCoordinator::new(config);
820
821 for _ in 0..5 {
822 let id = coordinator
823 .trigger_checkpoint()
824 .await
825 .expect("Checkpoint trigger should succeed in loop");
826 coordinator
827 .complete_checkpoint(id, true)
828 .await
829 .expect("Checkpoint completion should succeed in loop");
830 }
831
832 assert_eq!(coordinator.completed_count().await, 5);
833
834 coordinator.clear_old_checkpoints(2).await;
835 assert_eq!(coordinator.completed_count().await, 2);
836 }
837}