Skip to main content

prodigy/cook/execution/mapreduce/checkpoint/effects/
storage.rs

1//! Checkpoint storage effects
2//!
3//! This module provides Effect-based operations for checkpoint storage.
4//! Effects encapsulate I/O operations and enable composition, testing, and
5//! dependency injection via the Reader pattern.
6
7use crate::cook::execution::mapreduce::checkpoint::pure::preparation;
8use crate::cook::execution::mapreduce::checkpoint::{
9    CheckpointReason, CheckpointStorage, MapReduceCheckpoint,
10};
11use std::path::PathBuf;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14
15/// Environment for checkpoint storage effects
16pub trait CheckpointStorageEnv: Clone + Send + Sync {
17    /// Get the checkpoint storage implementation
18    fn storage(&self) -> Arc<dyn CheckpointStorage>;
19
20    /// Get the current checkpoint state
21    fn current_checkpoint(&self) -> Arc<RwLock<Option<MapReduceCheckpoint>>>;
22
23    /// Get the storage path for checkpoints
24    fn storage_path(&self) -> PathBuf;
25}
26
27/// Error during checkpoint storage operations
28#[derive(Debug, Clone, thiserror::Error)]
29pub enum CheckpointStorageError {
30    #[error("No checkpoint to save")]
31    NoCheckpoint,
32
33    #[error("Failed to save checkpoint: {0}")]
34    SaveFailed(String),
35
36    #[error("Failed to load checkpoint: {0}")]
37    LoadFailed(String),
38
39    #[error("Checkpoint not found: {0}")]
40    NotFound(String),
41}
42
43/// Save the current checkpoint
44///
45/// This function:
46/// 1. Reads the current checkpoint from state
47/// 2. Prepares it for saving (update timestamps, reset in-progress items)
48/// 3. Saves to storage
49pub async fn save_checkpoint<E: CheckpointStorageEnv>(
50    env: &E,
51    reason: CheckpointReason,
52) -> Result<String, CheckpointStorageError> {
53    let storage = env.storage();
54    let current_checkpoint = env.current_checkpoint();
55
56    // Read current checkpoint
57    let checkpoint_guard = current_checkpoint.read().await;
58    let checkpoint = match checkpoint_guard.as_ref() {
59        Some(cp) => cp.clone(),
60        None => return Err(CheckpointStorageError::NoCheckpoint),
61    };
62    drop(checkpoint_guard);
63
64    // Prepare checkpoint for saving (pure function)
65    let prepared = preparation::prepare_checkpoint(&checkpoint, reason);
66    let checkpoint_id = prepared.metadata.checkpoint_id.clone();
67
68    // Save to storage
69    storage
70        .save_checkpoint(&prepared)
71        .await
72        .map_err(|e| CheckpointStorageError::SaveFailed(e.to_string()))?;
73
74    Ok(checkpoint_id)
75}
76
77/// Load a checkpoint by ID
78pub async fn load_checkpoint<E: CheckpointStorageEnv>(
79    env: &E,
80    checkpoint_id: String,
81) -> Result<MapReduceCheckpoint, CheckpointStorageError> {
82    use crate::cook::execution::mapreduce::checkpoint::CheckpointId;
83
84    let storage = env.storage();
85    let id = CheckpointId::from_string(checkpoint_id);
86
87    storage
88        .load_checkpoint(&id)
89        .await
90        .map_err(|e| CheckpointStorageError::LoadFailed(e.to_string()))
91}
92
93/// Update the current checkpoint state
94pub async fn update_checkpoint_state<E: CheckpointStorageEnv>(
95    env: &E,
96    checkpoint: MapReduceCheckpoint,
97) -> Result<(), CheckpointStorageError> {
98    let current_checkpoint = env.current_checkpoint();
99    let mut guard = current_checkpoint.write().await;
100    *guard = Some(checkpoint);
101    Ok(())
102}
103
104/// Check if a checkpoint should be created
105///
106/// Uses the checkpoint trigger configuration and state
107/// to determine if a new checkpoint should be saved.
108pub fn should_save_checkpoint(
109    items_since_last: usize,
110    last_checkpoint_time: chrono::DateTime<chrono::Utc>,
111    config: &super::super::pure::triggers::CheckpointTriggerConfig,
112) -> bool {
113    use super::super::pure::triggers::should_checkpoint;
114
115    should_checkpoint(
116        items_since_last,
117        last_checkpoint_time,
118        chrono::Utc::now(),
119        config,
120    )
121}
122
123// Effect-based wrappers for composition with stillwater
124// Note: These are placeholder functions for future Effect integration.
125// The async functions above can be used directly until Effect patterns are finalized.
126
127/// Create an effect-like wrapper that saves the current checkpoint
128/// This is a convenience function that returns a future.
129pub async fn save_checkpoint_effect<E: CheckpointStorageEnv + Clone + 'static>(
130    env: E,
131    reason: CheckpointReason,
132) -> Result<String, CheckpointStorageError> {
133    save_checkpoint(&env, reason).await
134}
135
136/// Create an effect-like wrapper that loads a checkpoint by ID
137/// This is a convenience function that returns a future.
138pub async fn load_checkpoint_effect<E: CheckpointStorageEnv + Clone + 'static>(
139    env: E,
140    checkpoint_id: String,
141) -> Result<MapReduceCheckpoint, CheckpointStorageError> {
142    load_checkpoint(&env, checkpoint_id).await
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::cook::execution::mapreduce::checkpoint::{
149        FileCheckpointStorage, MapReduceCheckpoint, PhaseType,
150    };
151    use std::path::PathBuf;
152    use tempfile::TempDir;
153
154    // Mock environment for testing
155    #[derive(Clone)]
156    struct MockStorageEnv {
157        storage: Arc<dyn CheckpointStorage>,
158        current_checkpoint: Arc<RwLock<Option<MapReduceCheckpoint>>>,
159        storage_path: PathBuf,
160    }
161
162    impl CheckpointStorageEnv for MockStorageEnv {
163        fn storage(&self) -> Arc<dyn CheckpointStorage> {
164            Arc::clone(&self.storage)
165        }
166
167        fn current_checkpoint(&self) -> Arc<RwLock<Option<MapReduceCheckpoint>>> {
168            Arc::clone(&self.current_checkpoint)
169        }
170
171        fn storage_path(&self) -> PathBuf {
172            self.storage_path.clone()
173        }
174    }
175
176    fn create_test_env(temp_dir: &TempDir) -> MockStorageEnv {
177        let storage_path = temp_dir.path().to_path_buf();
178        let storage: Arc<dyn CheckpointStorage> =
179            Arc::new(FileCheckpointStorage::new(storage_path.clone(), true));
180        let checkpoint =
181            crate::cook::execution::mapreduce::checkpoint::pure::preparation::create_initial_checkpoint(
182                "test-job",
183                10,
184                PhaseType::Map,
185            );
186
187        MockStorageEnv {
188            storage,
189            current_checkpoint: Arc::new(RwLock::new(Some(checkpoint))),
190            storage_path,
191        }
192    }
193
194    #[tokio::test]
195    async fn test_save_checkpoint_no_checkpoint() {
196        let temp_dir = tempfile::tempdir().unwrap();
197        let mut env = create_test_env(&temp_dir);
198        env.current_checkpoint = Arc::new(RwLock::new(None));
199
200        let result = save_checkpoint(&env, CheckpointReason::Interval).await;
201
202        assert!(matches!(result, Err(CheckpointStorageError::NoCheckpoint)));
203    }
204
205    #[tokio::test]
206    async fn test_save_checkpoint_success() {
207        let temp_dir = tempfile::tempdir().unwrap();
208        let env = create_test_env(&temp_dir);
209
210        let result = save_checkpoint(&env, CheckpointReason::Interval).await;
211
212        assert!(result.is_ok());
213        assert!(result.unwrap().starts_with("cp-"));
214    }
215
216    #[tokio::test]
217    async fn test_update_checkpoint_state() {
218        let temp_dir = tempfile::tempdir().unwrap();
219        let env = create_test_env(&temp_dir);
220
221        // Create a new checkpoint to update
222        let new_checkpoint =
223            crate::cook::execution::mapreduce::checkpoint::pure::preparation::create_initial_checkpoint(
224                "new-job",
225                20,
226                PhaseType::Reduce,
227            );
228
229        let result = update_checkpoint_state(&env, new_checkpoint).await;
230
231        assert!(result.is_ok());
232
233        // Verify state was updated
234        let guard = env.current_checkpoint.read().await;
235        let checkpoint = guard.as_ref().unwrap();
236        assert_eq!(checkpoint.metadata.job_id, "new-job");
237        assert_eq!(checkpoint.metadata.total_work_items, 20);
238    }
239
240    #[tokio::test]
241    async fn test_should_save_checkpoint_function() {
242        use super::super::super::pure::triggers::CheckpointTriggerConfig;
243
244        let config = CheckpointTriggerConfig::item_interval(5);
245        let now = chrono::Utc::now();
246
247        // Below threshold
248        assert!(!should_save_checkpoint(3, now, &config));
249
250        // At threshold
251        assert!(should_save_checkpoint(5, now, &config));
252    }
253}