Skip to main content

dbx_core/engine/
rollback.rs

1// Phase 0.3: 롤백 메커니즘
2//
3// TDD 방식으로 구현:
4// 1. Red: 테스트 작성 (실패)
5// 2. Green: 최소 구현 (통과)
6// 3. Refactor: 코드 개선
7
8use crate::error::{DbxError, DbxResult};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::PathBuf;
13use std::sync::{Arc, RwLock};
14use std::time::{SystemTime, UNIX_EPOCH};
15
16/// 체크포인트
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Checkpoint {
19    /// 체크포인트 ID
20    pub id: String,
21
22    /// 생성 시간 (Unix timestamp)
23    pub timestamp: i64,
24
25    /// 설명
26    pub description: String,
27
28    /// 상태 데이터 (JSON)
29    pub state: HashMap<String, serde_json::Value>,
30}
31
32impl Checkpoint {
33    /// 새 체크포인트 생성
34    pub fn new(id: String, description: String) -> Self {
35        Self {
36            id,
37            timestamp: SystemTime::now()
38                .duration_since(UNIX_EPOCH)
39                .unwrap()
40                .as_secs() as i64,
41            description,
42            state: HashMap::new(),
43        }
44    }
45
46    /// 상태 데이터 추가
47    pub fn add_state<T: Serialize>(&mut self, key: String, value: &T) -> DbxResult<()> {
48        let json_value = serde_json::to_value(value)?;
49        self.state.insert(key, json_value);
50        Ok(())
51    }
52
53    /// 상태 데이터 조회
54    pub fn get_state<T: for<'de> Deserialize<'de>>(&self, key: &str) -> DbxResult<T> {
55        let json_value = self
56            .state
57            .get(key)
58            .ok_or_else(|| DbxError::Serialization(format!("State key '{}' not found", key)))?;
59
60        let value = serde_json::from_value(json_value.clone())?;
61        Ok(value)
62    }
63}
64
65/// 롤백 관리자
66pub struct RollbackManager {
67    /// 체크포인트 (ID → Checkpoint)
68    checkpoints: Arc<RwLock<HashMap<String, Checkpoint>>>,
69
70    /// 체크포인트 디렉토리
71    checkpoint_dir: PathBuf,
72
73    /// 자동 롤백 활성화
74    auto_rollback_enabled: bool,
75}
76
77impl RollbackManager {
78    /// 새 롤백 관리자 생성
79    pub fn new() -> Self {
80        Self {
81            checkpoints: Arc::new(RwLock::new(HashMap::new())),
82            checkpoint_dir: PathBuf::from("target/checkpoints"),
83            auto_rollback_enabled: false,
84        }
85    }
86
87    /// 체크포인트 디렉토리 설정
88    pub fn with_checkpoint_dir(mut self, dir: PathBuf) -> Self {
89        self.checkpoint_dir = dir;
90        self
91    }
92
93    /// 자동 롤백 활성화
94    pub fn with_auto_rollback(mut self, enabled: bool) -> Self {
95        self.auto_rollback_enabled = enabled;
96        self
97    }
98
99    /// 체크포인트 생성
100    pub fn create_checkpoint(&self, id: String, description: String) -> DbxResult<Checkpoint> {
101        let checkpoint = Checkpoint::new(id.clone(), description);
102
103        // 메모리에 저장
104        self.checkpoints
105            .write()
106            .unwrap()
107            .insert(id.clone(), checkpoint.clone());
108
109        // 파일에 저장
110        self.save_checkpoint(&checkpoint)?;
111
112        Ok(checkpoint)
113    }
114
115    /// 체크포인트 저장
116    fn save_checkpoint(&self, checkpoint: &Checkpoint) -> DbxResult<()> {
117        // 디렉토리 생성
118        fs::create_dir_all(&self.checkpoint_dir)?;
119
120        // 파일 경로
121        let file_path = self.checkpoint_dir.join(format!("{}.json", checkpoint.id));
122
123        // JSON 직렬화
124        let json = serde_json::to_string_pretty(checkpoint)?;
125
126        // 파일 쓰기
127        fs::write(file_path, json)?;
128
129        Ok(())
130    }
131
132    /// 체크포인트 로드
133    fn load_checkpoint(&self, id: &str) -> DbxResult<Checkpoint> {
134        let file_path = self.checkpoint_dir.join(format!("{}.json", id));
135
136        if !file_path.exists() {
137            return Err(DbxError::Serialization(format!(
138                "Checkpoint '{}' not found",
139                id
140            )));
141        }
142
143        let json = fs::read_to_string(file_path)?;
144        let checkpoint: Checkpoint = serde_json::from_str(&json)?;
145
146        Ok(checkpoint)
147    }
148
149    /// 체크포인트로 롤백
150    pub fn rollback_to_checkpoint(&self, id: &str) -> DbxResult<Checkpoint> {
151        // 파일에서 로드
152        let checkpoint = self.load_checkpoint(id)?;
153
154        // 메모리에 저장
155        self.checkpoints
156            .write()
157            .unwrap()
158            .insert(id.to_string(), checkpoint.clone());
159
160        Ok(checkpoint)
161    }
162
163    /// 체크포인트 조회
164    pub fn get_checkpoint(&self, id: &str) -> Option<Checkpoint> {
165        self.checkpoints.read().unwrap().get(id).cloned()
166    }
167
168    /// 모든 체크포인트 조회
169    pub fn list_checkpoints(&self) -> Vec<Checkpoint> {
170        self.checkpoints.read().unwrap().values().cloned().collect()
171    }
172
173    /// 체크포인트 삭제
174    pub fn delete_checkpoint(&self, id: &str) -> DbxResult<()> {
175        // 메모리에서 삭제
176        self.checkpoints.write().unwrap().remove(id);
177
178        // 파일 삭제
179        let file_path = self.checkpoint_dir.join(format!("{}.json", id));
180        if file_path.exists() {
181            fs::remove_file(file_path)?;
182        }
183
184        Ok(())
185    }
186
187    /// 자동 롤백 트리거
188    pub fn trigger_auto_rollback(&self, reason: &str) -> DbxResult<()> {
189        if !self.auto_rollback_enabled {
190            return Ok(());
191        }
192
193        // 가장 최근 체크포인트로 롤백
194        let checkpoints = self.list_checkpoints();
195        if let Some(latest) = checkpoints.iter().max_by_key(|c| c.timestamp) {
196            eprintln!("Auto-rollback triggered: {}", reason);
197            eprintln!("Rolling back to checkpoint: {}", latest.id);
198            self.rollback_to_checkpoint(&latest.id)?;
199        }
200
201        Ok(())
202    }
203}
204
205impl Default for RollbackManager {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    // TDD: Red - 테스트 작성 (실패)
216
217    #[test]
218    fn test_checkpoint_creation() {
219        let manager =
220            RollbackManager::new().with_checkpoint_dir(PathBuf::from("target/test_checkpoints"));
221
222        let checkpoint = manager
223            .create_checkpoint("test_cp_1".to_string(), "Test checkpoint".to_string())
224            .unwrap();
225
226        assert_eq!(checkpoint.id, "test_cp_1");
227        assert_eq!(checkpoint.description, "Test checkpoint");
228        assert!(checkpoint.timestamp > 0);
229
230        // 조회 확인
231        let loaded = manager.get_checkpoint("test_cp_1");
232        assert!(loaded.is_some());
233
234        // 정리
235        let _ = manager.delete_checkpoint("test_cp_1");
236    }
237
238    #[test]
239    fn test_rollback_to_checkpoint() {
240        let manager =
241            RollbackManager::new().with_checkpoint_dir(PathBuf::from("target/test_checkpoints"));
242
243        // 체크포인트 생성
244        let mut checkpoint = manager
245            .create_checkpoint("test_cp_2".to_string(), "Rollback test".to_string())
246            .unwrap();
247
248        // 상태 데이터 추가
249        checkpoint
250            .add_state("key1".to_string(), &"value1".to_string())
251            .unwrap();
252        checkpoint.add_state("key2".to_string(), &42).unwrap();
253
254        // 저장
255        manager
256            .checkpoints
257            .write()
258            .unwrap()
259            .insert("test_cp_2".to_string(), checkpoint.clone());
260        manager.save_checkpoint(&checkpoint).unwrap();
261
262        // 메모리 초기화
263        manager.checkpoints.write().unwrap().clear();
264
265        // 롤백
266        let restored = manager.rollback_to_checkpoint("test_cp_2").unwrap();
267
268        // 확인
269        assert_eq!(restored.id, "test_cp_2");
270        let value1: String = restored.get_state("key1").unwrap();
271        let value2: i32 = restored.get_state("key2").unwrap();
272        assert_eq!(value1, "value1");
273        assert_eq!(value2, 42);
274
275        // 정리
276        let _ = manager.delete_checkpoint("test_cp_2");
277    }
278
279    #[test]
280    fn test_auto_rollback_on_regression() {
281        let manager = RollbackManager::new()
282            .with_checkpoint_dir(PathBuf::from("target/test_checkpoints"))
283            .with_auto_rollback(true);
284
285        // 체크포인트 생성
286        manager
287            .create_checkpoint("test_cp_3".to_string(), "Auto-rollback test".to_string())
288            .unwrap();
289
290        // 자동 롤백 트리거
291        manager
292            .trigger_auto_rollback("Performance regression detected")
293            .unwrap();
294
295        // 확인
296        let checkpoint = manager.get_checkpoint("test_cp_3");
297        assert!(checkpoint.is_some());
298
299        // 정리
300        let _ = manager.delete_checkpoint("test_cp_3");
301    }
302}