Skip to main content

alopex_server/ops/
state.rs

1use std::sync::RwLock;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{Result, ServerError};
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10pub enum Mode {
11    Normal,
12    ReadOnly,
13    Maintenance,
14}
15
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "snake_case")]
18pub enum OperationStatus {
19    Queued,
20    Running,
21    Completed,
22    Failed,
23    Cancelled,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27pub struct Progress {
28    pub percent: Option<u8>,
29    pub bytes_processed: Option<u64>,
30}
31
32impl Progress {
33    pub fn percent(value: u8) -> Self {
34        Self {
35            percent: Some(value),
36            bytes_processed: None,
37        }
38    }
39
40    pub fn bytes(value: u64) -> Self {
41        Self {
42            percent: None,
43            bytes_processed: Some(value),
44        }
45    }
46
47    pub fn validate(&self) -> Result<()> {
48        if self.percent.is_none() && self.bytes_processed.is_none() {
49            return Err(ServerError::BadRequest(
50                "progress must include percent or bytes_processed".to_string(),
51            ));
52        }
53        Ok(())
54    }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58pub struct OperationState {
59    pub status: OperationStatus,
60    pub started_at_ms: Option<u64>,
61    pub finished_at_ms: Option<u64>,
62    pub progress: Option<Progress>,
63    pub reason: Option<String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct RestoreMetadata {
68    pub backup_id: String,
69    pub location: String,
70    pub restored_at_ms: u64,
71    pub size_bytes: u64,
72}
73
74impl OperationState {
75    pub fn queued() -> Self {
76        Self {
77            status: OperationStatus::Queued,
78            started_at_ms: None,
79            finished_at_ms: None,
80            progress: None,
81            reason: None,
82        }
83    }
84
85    pub fn running() -> Self {
86        Self {
87            status: OperationStatus::Running,
88            started_at_ms: Some(now_ms()),
89            finished_at_ms: None,
90            progress: None,
91            reason: None,
92        }
93    }
94
95    pub fn completed(progress: Option<Progress>) -> Result<Self> {
96        if let Some(progress) = &progress {
97            progress.validate()?;
98        }
99        Ok(Self {
100            status: OperationStatus::Completed,
101            started_at_ms: None,
102            finished_at_ms: Some(now_ms()),
103            progress,
104            reason: None,
105        })
106    }
107
108    pub fn failed(reason: impl Into<String>) -> Self {
109        Self {
110            status: OperationStatus::Failed,
111            started_at_ms: None,
112            finished_at_ms: Some(now_ms()),
113            progress: None,
114            reason: Some(reason.into()),
115        }
116    }
117
118    pub fn cancelled(reason: impl Into<String>) -> Self {
119        Self {
120            status: OperationStatus::Cancelled,
121            started_at_ms: None,
122            finished_at_ms: Some(now_ms()),
123            progress: None,
124            reason: Some(reason.into()),
125        }
126    }
127
128    pub fn mark_running(&mut self) {
129        self.status = OperationStatus::Running;
130        self.started_at_ms = Some(now_ms());
131        self.finished_at_ms = None;
132    }
133
134    pub fn mark_finished(&mut self, status: OperationStatus, reason: Option<String>) {
135        self.status = status;
136        self.finished_at_ms = Some(now_ms());
137        self.reason = reason;
138    }
139
140    pub fn set_progress(&mut self, progress: Progress) -> Result<()> {
141        progress.validate()?;
142        self.progress = Some(progress);
143        Ok(())
144    }
145}
146
147#[derive(Debug, Clone)]
148struct LifecycleState {
149    mode: Mode,
150    backup_state: OperationState,
151    restore_state: OperationState,
152    restore_metadata: Option<RestoreMetadata>,
153}
154
155#[derive(Debug)]
156pub struct LifecycleStateManager {
157    inner: RwLock<LifecycleState>,
158}
159
160impl LifecycleStateManager {
161    pub fn new(initial_mode: Mode) -> Self {
162        Self {
163            inner: RwLock::new(LifecycleState {
164                mode: initial_mode,
165                backup_state: OperationState::queued(),
166                restore_state: OperationState::queued(),
167                restore_metadata: None,
168            }),
169        }
170    }
171
172    pub fn current_mode(&self) -> Mode {
173        self.inner
174            .read()
175            .expect("lifecycle state lock poisoned")
176            .mode
177    }
178
179    pub fn set_mode(&self, mode: Mode) {
180        self.inner
181            .write()
182            .expect("lifecycle state lock poisoned")
183            .mode = mode;
184    }
185
186    pub fn backup_state(&self) -> OperationState {
187        self.inner
188            .read()
189            .expect("lifecycle state lock poisoned")
190            .backup_state
191            .clone()
192    }
193
194    pub fn set_backup_state(&self, state: OperationState) {
195        self.inner
196            .write()
197            .expect("lifecycle state lock poisoned")
198            .backup_state = state;
199    }
200
201    pub fn restore_state(&self) -> OperationState {
202        self.inner
203            .read()
204            .expect("lifecycle state lock poisoned")
205            .restore_state
206            .clone()
207    }
208
209    pub fn set_restore_state(&self, state: OperationState) {
210        self.inner
211            .write()
212            .expect("lifecycle state lock poisoned")
213            .restore_state = state;
214    }
215
216    pub fn restore_metadata(&self) -> Option<RestoreMetadata> {
217        self.inner
218            .read()
219            .expect("lifecycle state lock poisoned")
220            .restore_metadata
221            .clone()
222    }
223
224    pub fn set_restore_metadata(&self, metadata: Option<RestoreMetadata>) {
225        self.inner
226            .write()
227            .expect("lifecycle state lock poisoned")
228            .restore_metadata = metadata;
229    }
230
231    pub fn should_block_writes(&self) -> bool {
232        matches!(self.current_mode(), Mode::ReadOnly | Mode::Maintenance)
233    }
234
235    pub fn check_write_allowed(&self) -> Result<()> {
236        match self.current_mode() {
237            Mode::Normal => Ok(()),
238            Mode::ReadOnly => Err(ServerError::Conflict(
239                "writes are blocked in read_only mode".to_string(),
240            )),
241            Mode::Maintenance => Err(ServerError::Conflict(
242                "writes are blocked in maintenance mode".to_string(),
243            )),
244        }
245    }
246}
247
248fn now_ms() -> u64 {
249    SystemTime::now()
250        .duration_since(UNIX_EPOCH)
251        .unwrap_or_default()
252        .as_millis() as u64
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn progress_validation_rejects_empty() {
261        let invalid = Progress {
262            percent: None,
263            bytes_processed: None,
264        };
265        assert!(invalid.validate().is_err());
266        let mut state = OperationState::running();
267        assert!(state.set_progress(invalid).is_err());
268    }
269
270    #[test]
271    fn progress_validation_accepts_percent_and_bytes() {
272        let percent = Progress::percent(10);
273        assert!(percent.validate().is_ok());
274        let bytes = Progress::bytes(2048);
275        assert!(bytes.validate().is_ok());
276    }
277
278    #[test]
279    fn lifecycle_write_guard_blocks_non_normal_modes() {
280        let manager = LifecycleStateManager::new(Mode::Normal);
281        assert_eq!(manager.current_mode(), Mode::Normal);
282        assert!(!manager.should_block_writes());
283        assert!(manager.check_write_allowed().is_ok());
284
285        manager.set_mode(Mode::ReadOnly);
286        assert!(manager.should_block_writes());
287        assert!(manager.check_write_allowed().is_err());
288
289        manager.set_mode(Mode::Maintenance);
290        assert!(manager.should_block_writes());
291        assert!(manager.check_write_allowed().is_err());
292    }
293
294    #[test]
295    fn operation_state_transitions_capture_timestamps() {
296        let mut state = OperationState::queued();
297        assert_eq!(state.status, OperationStatus::Queued);
298        assert!(state.started_at_ms.is_none());
299        assert!(state.finished_at_ms.is_none());
300
301        state.mark_running();
302        assert_eq!(state.status, OperationStatus::Running);
303        assert!(state.started_at_ms.is_some());
304        assert!(state.finished_at_ms.is_none());
305
306        state.mark_finished(OperationStatus::Completed, None);
307        assert_eq!(state.status, OperationStatus::Completed);
308        assert!(state.finished_at_ms.is_some());
309    }
310
311    #[test]
312    fn restore_metadata_is_stored_and_loaded() {
313        let manager = LifecycleStateManager::new(Mode::Normal);
314        let metadata = RestoreMetadata {
315            backup_id: "backup-1".to_string(),
316            location: "/tmp/backup".to_string(),
317            restored_at_ms: 1234,
318            size_bytes: 512,
319        };
320        manager.set_restore_metadata(Some(metadata.clone()));
321        assert_eq!(manager.restore_metadata(), Some(metadata));
322        manager.set_restore_metadata(None);
323        assert!(manager.restore_metadata().is_none());
324    }
325}