prodigy/cook/execution/mapreduce/checkpoint/effects/
storage.rs1use 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
15pub trait CheckpointStorageEnv: Clone + Send + Sync {
17 fn storage(&self) -> Arc<dyn CheckpointStorage>;
19
20 fn current_checkpoint(&self) -> Arc<RwLock<Option<MapReduceCheckpoint>>>;
22
23 fn storage_path(&self) -> PathBuf;
25}
26
27#[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
43pub 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 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 let prepared = preparation::prepare_checkpoint(&checkpoint, reason);
66 let checkpoint_id = prepared.metadata.checkpoint_id.clone();
67
68 storage
70 .save_checkpoint(&prepared)
71 .await
72 .map_err(|e| CheckpointStorageError::SaveFailed(e.to_string()))?;
73
74 Ok(checkpoint_id)
75}
76
77pub 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
93pub 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
104pub 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
123pub 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
136pub 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 #[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 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 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 assert!(!should_save_checkpoint(3, now, &config));
249
250 assert!(should_save_checkpoint(5, now, &config));
252 }
253}