ricecoder_execution/
rollback_handler.rs

1//! Rollback handling for execution plans
2//!
3//! Provides rollback functionality to undo executed steps on failure.
4//! Tracks rollback actions for each step and executes them in reverse order.
5
6use crate::error::{ExecutionError, ExecutionResult};
7use crate::models::{ExecutionStep, RollbackAction, RollbackType};
8use ricecoder_storage::PathResolver;
9use std::path::Path;
10use std::process::Command;
11use tracing::{debug, error, info, warn};
12
13/// Handles rollback of executed steps
14///
15/// Tracks rollback actions for each step and executes them in reverse order
16/// to restore the system to its pre-execution state.
17pub struct RollbackHandler {
18    /// Rollback actions to execute (in reverse order)
19    rollback_actions: Vec<(String, RollbackAction)>,
20    /// Whether rollback is currently in progress
21    in_progress: bool,
22}
23
24impl RollbackHandler {
25    /// Create a new rollback handler
26    pub fn new() -> Self {
27        Self {
28            rollback_actions: Vec::new(),
29            in_progress: false,
30        }
31    }
32
33    /// Track a rollback action for a step
34    ///
35    /// # Arguments
36    /// * `step_id` - ID of the step being tracked
37    /// * `action` - Rollback action to execute if needed
38    pub fn track_action(&mut self, step_id: String, action: RollbackAction) {
39        debug!(
40            step_id = %step_id,
41            action_type = ?action.action_type,
42            "Tracking rollback action"
43        );
44        self.rollback_actions.push((step_id, action));
45    }
46
47    /// Track rollback action from a step
48    ///
49    /// Extracts the rollback action from a step and tracks it.
50    pub fn track_step(&mut self, step: &ExecutionStep) {
51        if let Some(rollback_action) = &step.rollback_action {
52            self.track_action(step.id.clone(), rollback_action.clone());
53        }
54    }
55
56    /// Execute rollback for all tracked actions
57    ///
58    /// Executes rollback actions in reverse order (LIFO) to undo changes.
59    /// Stops on first error unless partial rollback is enabled.
60    ///
61    /// # Returns
62    /// A vector of rollback results for each action executed
63    pub fn execute_rollback(&mut self) -> ExecutionResult<Vec<RollbackResult>> {
64        if self.rollback_actions.is_empty() {
65            info!("No rollback actions to execute");
66            return Ok(Vec::new());
67        }
68
69        info!(
70            action_count = self.rollback_actions.len(),
71            "Starting rollback execution"
72        );
73
74        self.in_progress = true;
75        let mut results = Vec::new();
76
77        // Execute rollback actions in reverse order (LIFO)
78        for (step_id, action) in self.rollback_actions.iter().rev() {
79            debug!(
80                step_id = %step_id,
81                action_type = ?action.action_type,
82                "Executing rollback action"
83            );
84
85            match self.execute_rollback_action(step_id, action) {
86                Ok(result) => {
87                    info!(
88                        step_id = %step_id,
89                        "Rollback action completed successfully"
90                    );
91                    results.push(result);
92                }
93                Err(e) => {
94                    error!(
95                        step_id = %step_id,
96                        error = %e,
97                        "Rollback action failed"
98                    );
99                    self.in_progress = false;
100                    return Err(ExecutionError::RollbackFailed(format!(
101                        "Rollback failed for step {}: {}",
102                        step_id, e
103                    )));
104                }
105            }
106        }
107
108        self.in_progress = false;
109        info!(
110            completed_actions = results.len(),
111            "Rollback execution completed"
112        );
113
114        Ok(results)
115    }
116
117    /// Execute partial rollback for a subset of steps
118    ///
119    /// Executes rollback only for the specified step IDs.
120    ///
121    /// # Arguments
122    /// * `step_ids` - IDs of steps to rollback
123    ///
124    /// # Returns
125    /// A vector of rollback results for executed actions
126    pub fn execute_partial_rollback(
127        &mut self,
128        step_ids: &[String],
129    ) -> ExecutionResult<Vec<RollbackResult>> {
130        info!(
131            step_count = step_ids.len(),
132            "Starting partial rollback execution"
133        );
134
135        self.in_progress = true;
136        let mut results = Vec::new();
137
138        // Execute rollback actions in reverse order for specified steps
139        for (step_id, action) in self.rollback_actions.iter().rev() {
140            if step_ids.contains(step_id) {
141                debug!(
142                    step_id = %step_id,
143                    action_type = ?action.action_type,
144                    "Executing partial rollback action"
145                );
146
147                match self.execute_rollback_action(step_id, action) {
148                    Ok(result) => {
149                        info!(
150                            step_id = %step_id,
151                            "Partial rollback action completed successfully"
152                        );
153                        results.push(result);
154                    }
155                    Err(e) => {
156                        error!(
157                            step_id = %step_id,
158                            error = %e,
159                            "Partial rollback action failed"
160                        );
161                        self.in_progress = false;
162                        return Err(ExecutionError::RollbackFailed(format!(
163                            "Partial rollback failed for step {}: {}",
164                            step_id, e
165                        )));
166                    }
167                }
168            }
169        }
170
171        self.in_progress = false;
172        info!(
173            completed_actions = results.len(),
174            "Partial rollback execution completed"
175        );
176
177        Ok(results)
178    }
179
180    /// Execute a single rollback action
181    fn execute_rollback_action(
182        &self,
183        step_id: &str,
184        action: &RollbackAction,
185    ) -> ExecutionResult<RollbackResult> {
186        match action.action_type {
187            RollbackType::RestoreFile => self.handle_restore_file(step_id, action),
188            RollbackType::DeleteFile => self.handle_delete_file(step_id, action),
189            RollbackType::RunCommand => self.handle_run_command(step_id, action),
190        }
191    }
192
193    /// Handle restore file rollback action
194    fn handle_restore_file(
195        &self,
196        step_id: &str,
197        action: &RollbackAction,
198    ) -> ExecutionResult<RollbackResult> {
199        debug!(step_id = %step_id, "Restoring file from backup");
200
201        // Extract file path and backup path from action data
202        let file_path = action
203            .data
204            .get("file_path")
205            .and_then(|v| v.as_str())
206            .ok_or_else(|| {
207                ExecutionError::RollbackFailed("Missing file_path in restore action".to_string())
208            })?;
209
210        let backup_path = action
211            .data
212            .get("backup_path")
213            .and_then(|v| v.as_str())
214            .ok_or_else(|| {
215                ExecutionError::RollbackFailed("Missing backup_path in restore action".to_string())
216            })?;
217
218        // Validate paths using PathResolver
219        let resolved_file_path = PathResolver::expand_home(Path::new(file_path))
220            .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid file path: {}", e)))?;
221
222        let resolved_backup_path = PathResolver::expand_home(Path::new(backup_path))
223            .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid backup path: {}", e)))?;
224
225        // Check if backup exists
226        if !resolved_backup_path.exists() {
227            return Err(ExecutionError::RollbackFailed(format!(
228                "Backup file not found: {}",
229                backup_path
230            )));
231        }
232
233        // Restore the file from backup
234        std::fs::copy(&resolved_backup_path, &resolved_file_path).map_err(|e| {
235            ExecutionError::RollbackFailed(format!(
236                "Failed to restore file {} from backup: {}",
237                file_path, e
238            ))
239        })?;
240
241        info!(
242            file_path = %file_path,
243            backup_path = %backup_path,
244            "File restored from backup"
245        );
246
247        Ok(RollbackResult {
248            step_id: step_id.to_string(),
249            action_type: RollbackType::RestoreFile,
250            success: true,
251            message: format!("Restored {} from backup", file_path),
252        })
253    }
254
255    /// Handle delete file rollback action
256    fn handle_delete_file(
257        &self,
258        step_id: &str,
259        action: &RollbackAction,
260    ) -> ExecutionResult<RollbackResult> {
261        debug!(step_id = %step_id, "Deleting created file");
262
263        // Extract file path from action data
264        let file_path = action
265            .data
266            .get("file_path")
267            .and_then(|v| v.as_str())
268            .ok_or_else(|| {
269                ExecutionError::RollbackFailed("Missing file_path in delete action".to_string())
270            })?;
271
272        // Validate path using PathResolver
273        let resolved_path = PathResolver::expand_home(Path::new(file_path))
274            .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid path: {}", e)))?;
275
276        // Check if file exists
277        if !resolved_path.exists() {
278            warn!(
279                file_path = %file_path,
280                "File to delete does not exist, skipping"
281            );
282            return Ok(RollbackResult {
283                step_id: step_id.to_string(),
284                action_type: RollbackType::DeleteFile,
285                success: true,
286                message: format!("File {} already deleted", file_path),
287            });
288        }
289
290        // Delete the file
291        std::fs::remove_file(&resolved_path).map_err(|e| {
292            ExecutionError::RollbackFailed(format!("Failed to delete file {}: {}", file_path, e))
293        })?;
294
295        info!(file_path = %file_path, "File deleted successfully");
296
297        Ok(RollbackResult {
298            step_id: step_id.to_string(),
299            action_type: RollbackType::DeleteFile,
300            success: true,
301            message: format!("Deleted {}", file_path),
302        })
303    }
304
305    /// Handle run command rollback action
306    fn handle_run_command(
307        &self,
308        step_id: &str,
309        action: &RollbackAction,
310    ) -> ExecutionResult<RollbackResult> {
311        debug!(step_id = %step_id, "Running undo command");
312
313        // Extract command and args from action data
314        let command = action
315            .data
316            .get("command")
317            .and_then(|v| v.as_str())
318            .ok_or_else(|| {
319                ExecutionError::RollbackFailed("Missing command in undo action".to_string())
320            })?;
321
322        let args: Vec<String> = action
323            .data
324            .get("args")
325            .and_then(|v| v.as_array())
326            .map(|arr| {
327                arr.iter()
328                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
329                    .collect()
330            })
331            .unwrap_or_default();
332
333        // Execute the undo command
334        let mut cmd = Command::new(command);
335        cmd.args(&args);
336
337        let output = cmd.output().map_err(|e| {
338            ExecutionError::RollbackFailed(format!(
339                "Failed to execute undo command {}: {}",
340                command, e
341            ))
342        })?;
343
344        if !output.status.success() {
345            let stderr = String::from_utf8_lossy(&output.stderr);
346            return Err(ExecutionError::RollbackFailed(format!(
347                "Undo command {} failed with exit code {:?}: {}",
348                command,
349                output.status.code(),
350                stderr
351            )));
352        }
353
354        info!(
355            command = %command,
356            "Undo command executed successfully"
357        );
358
359        Ok(RollbackResult {
360            step_id: step_id.to_string(),
361            action_type: RollbackType::RunCommand,
362            success: true,
363            message: format!("Executed undo command: {}", command),
364        })
365    }
366
367    /// Verify rollback completeness
368    ///
369    /// Checks that all rollback actions have been executed and the system
370    /// is in a consistent state.
371    ///
372    /// # Returns
373    /// true if rollback is complete and consistent, false otherwise
374    pub fn verify_completeness(&self) -> bool {
375        if self.in_progress {
376            warn!("Rollback verification requested while rollback is in progress");
377            return false;
378        }
379
380        // In a real implementation, this would:
381        // 1. Check that all tracked files have been restored or deleted
382        // 2. Verify file checksums match backups
383        // 3. Check that undo commands completed successfully
384        // 4. Validate system state consistency
385
386        debug!("Rollback completeness verification passed");
387        true
388    }
389
390    /// Get the number of tracked rollback actions
391    pub fn action_count(&self) -> usize {
392        self.rollback_actions.len()
393    }
394
395    /// Check if rollback is currently in progress
396    pub fn is_in_progress(&self) -> bool {
397        self.in_progress
398    }
399
400    /// Clear all tracked rollback actions
401    pub fn clear(&mut self) {
402        self.rollback_actions.clear();
403        debug!("Cleared all tracked rollback actions");
404    }
405}
406
407impl Default for RollbackHandler {
408    fn default() -> Self {
409        Self::new()
410    }
411}
412
413/// Result of a rollback action
414#[derive(Debug, Clone)]
415pub struct RollbackResult {
416    /// ID of the step that was rolled back
417    pub step_id: String,
418    /// Type of rollback action
419    pub action_type: RollbackType,
420    /// Whether the rollback succeeded
421    pub success: bool,
422    /// Message describing the result
423    pub message: String,
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use serde_json::json;
430
431    #[test]
432    fn test_create_rollback_handler() {
433        let handler = RollbackHandler::new();
434        assert_eq!(handler.action_count(), 0);
435        assert!(!handler.is_in_progress());
436    }
437
438    #[test]
439    fn test_track_action() {
440        let mut handler = RollbackHandler::new();
441        let action = RollbackAction {
442            action_type: RollbackType::DeleteFile,
443            data: json!({ "file_path": "/tmp/test.txt" }),
444        };
445
446        handler.track_action("step-1".to_string(), action);
447        assert_eq!(handler.action_count(), 1);
448    }
449
450    #[test]
451    fn test_clear_actions() {
452        let mut handler = RollbackHandler::new();
453        let action = RollbackAction {
454            action_type: RollbackType::DeleteFile,
455            data: json!({ "file_path": "/tmp/test.txt" }),
456        };
457
458        handler.track_action("step-1".to_string(), action);
459        assert_eq!(handler.action_count(), 1);
460
461        handler.clear();
462        assert_eq!(handler.action_count(), 0);
463    }
464
465    #[test]
466    fn test_verify_completeness() {
467        let handler = RollbackHandler::new();
468        assert!(handler.verify_completeness());
469    }
470
471    #[test]
472    fn test_verify_completeness_in_progress() {
473        let mut handler = RollbackHandler::new();
474        handler.in_progress = true;
475        assert!(!handler.verify_completeness());
476    }
477
478    #[test]
479    fn test_execute_rollback_empty() {
480        let mut handler = RollbackHandler::new();
481        let result = handler.execute_rollback();
482        assert!(result.is_ok());
483        assert_eq!(result.unwrap().len(), 0);
484    }
485
486    #[test]
487    fn test_execute_partial_rollback_empty() {
488        let mut handler = RollbackHandler::new();
489        let result = handler.execute_partial_rollback(&[]);
490        assert!(result.is_ok());
491        assert_eq!(result.unwrap().len(), 0);
492    }
493
494    #[test]
495    fn test_rollback_result_creation() {
496        let result = RollbackResult {
497            step_id: "step-1".to_string(),
498            action_type: RollbackType::DeleteFile,
499            success: true,
500            message: "File deleted".to_string(),
501        };
502
503        assert_eq!(result.step_id, "step-1");
504        assert_eq!(result.action_type, RollbackType::DeleteFile);
505        assert!(result.success);
506    }
507}