rust_docs_mcp/cache/
transaction.rs

1//! Transaction-like operations for crate caching with automatic rollback
2//!
3//! This module provides utilities for safely updating cached crates with
4//! automatic backup and restore capabilities.
5
6use anyhow::{Context, Result};
7use std::path::PathBuf;
8
9use crate::cache::storage::CacheStorage;
10
11/// Represents a cache update transaction with automatic rollback on failure
12pub struct CacheTransaction<'a> {
13    storage: &'a CacheStorage,
14    crate_name: String,
15    version: String,
16    backup_path: Option<PathBuf>,
17}
18
19impl<'a> CacheTransaction<'a> {
20    /// Create a new cache transaction
21    pub fn new(storage: &'a CacheStorage, crate_name: &str, version: &str) -> Self {
22        Self {
23            storage,
24            crate_name: crate_name.to_string(),
25            version: version.to_string(),
26            backup_path: None,
27        }
28    }
29
30    /// Begin the transaction by creating a backup if the crate exists
31    pub fn begin(&mut self) -> Result<()> {
32        if self.storage.is_cached(&self.crate_name, &self.version) {
33            let backup_path = self
34                .storage
35                .backup_crate_to_temp(&self.crate_name, &self.version)
36                .context("Failed to create backup")?;
37            self.backup_path = Some(backup_path);
38
39            // Remove the existing cache
40            self.storage
41                .remove_crate(&self.crate_name, &self.version)
42                .context("Failed to remove existing cache")?;
43        }
44        Ok(())
45    }
46
47    /// Commit the transaction by cleaning up the backup
48    pub fn commit(mut self) -> Result<()> {
49        if let Some(backup_path) = self.backup_path.take() {
50            // Cleanup is best-effort - the transaction succeeded even if cleanup fails
51            let _ = self.storage.cleanup_backup(&backup_path);
52        }
53        Ok(())
54    }
55
56    /// Rollback the transaction by restoring from backup
57    pub fn rollback(&mut self) -> Result<()> {
58        if let Some(backup_path) = self.backup_path.take() {
59            // Check if backup exists before trying to restore
60            if !backup_path.exists() {
61                anyhow::bail!(
62                    "Backup path does not exist: {}. Cannot rollback.",
63                    backup_path.display()
64                );
65            }
66
67            self.storage
68                .restore_crate_from_backup(&self.crate_name, &self.version, &backup_path)
69                .context("Failed to restore from backup")?;
70
71            // Cleanup is best-effort - don't fail if backup is already gone
72            let _ = self.storage.cleanup_backup(&backup_path);
73        }
74        Ok(())
75    }
76}
77
78impl<'a> Drop for CacheTransaction<'a> {
79    fn drop(&mut self) {
80        // If transaction wasn't committed and there's a backup, try to rollback
81        if self.backup_path.is_some() {
82            let _ = self.rollback();
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::fs;
91    use tempfile::TempDir;
92
93    #[test]
94    fn test_transaction_commit() -> Result<()> {
95        let temp_dir = TempDir::new()?;
96        let storage = CacheStorage::new(Some(temp_dir.path().to_path_buf()))?;
97
98        // Create a fake cached crate with proper structure
99        let source_path = storage.source_path("test-crate", "1.0.0")?;
100        storage.ensure_dir(&source_path)?;
101        fs::write(source_path.join("file.txt"), "original content")?;
102        storage.save_metadata("test-crate", "1.0.0")?;
103
104        // Start transaction
105        let mut transaction = CacheTransaction::new(&storage, "test-crate", "1.0.0");
106        transaction.begin()?;
107
108        // Verify crate was removed
109        assert!(!storage.is_cached("test-crate", "1.0.0"));
110
111        // Add new content
112        let new_source_path = storage.source_path("test-crate", "1.0.0")?;
113        storage.ensure_dir(&new_source_path)?;
114        fs::write(new_source_path.join("file.txt"), "new content")?;
115        storage.save_metadata("test-crate", "1.0.0")?;
116
117        // Commit transaction
118        transaction.commit()?;
119
120        // Verify new content exists
121        assert!(storage.is_cached("test-crate", "1.0.0"));
122        let content = fs::read_to_string(new_source_path.join("file.txt"))?;
123        assert_eq!(content, "new content");
124
125        Ok(())
126    }
127
128    #[test]
129    fn test_transaction_rollback() -> Result<()> {
130        let temp_dir = TempDir::new()?;
131        let storage = CacheStorage::new(Some(temp_dir.path().to_path_buf()))?;
132
133        // Create a fake cached crate with proper structure
134        let source_path = storage.source_path("test-crate", "1.0.0")?;
135        storage.ensure_dir(&source_path)?;
136        fs::write(source_path.join("file.txt"), "original content")?;
137
138        // Save metadata to make it a valid cached crate
139        storage.save_metadata("test-crate", "1.0.0")?;
140
141        // Verify the crate exists before transaction
142        assert!(storage.is_cached("test-crate", "1.0.0"));
143
144        // Start transaction
145        let mut transaction = CacheTransaction::new(&storage, "test-crate", "1.0.0");
146        transaction.begin()?;
147        // Verify crate was removed
148        assert!(!storage.is_cached("test-crate", "1.0.0"));
149
150        // Simulate failure - rollback
151        transaction.rollback()?;
152
153        // Verify original content was restored
154        assert!(storage.is_cached("test-crate", "1.0.0"));
155        let restored_source_path = storage.source_path("test-crate", "1.0.0")?;
156        let content = fs::read_to_string(restored_source_path.join("file.txt"))?;
157        assert_eq!(content, "original content");
158
159        Ok(())
160    }
161}