use std::sync::RwLock;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::error::{Result, ServerError};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
Normal,
ReadOnly,
Maintenance,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OperationStatus {
Queued,
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Progress {
pub percent: Option<u8>,
pub bytes_processed: Option<u64>,
}
impl Progress {
pub fn percent(value: u8) -> Self {
Self {
percent: Some(value),
bytes_processed: None,
}
}
pub fn bytes(value: u64) -> Self {
Self {
percent: None,
bytes_processed: Some(value),
}
}
pub fn validate(&self) -> Result<()> {
if self.percent.is_none() && self.bytes_processed.is_none() {
return Err(ServerError::BadRequest(
"progress must include percent or bytes_processed".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OperationState {
pub status: OperationStatus,
pub started_at_ms: Option<u64>,
pub finished_at_ms: Option<u64>,
pub progress: Option<Progress>,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RestoreMetadata {
pub backup_id: String,
pub location: String,
pub restored_at_ms: u64,
pub size_bytes: u64,
}
impl OperationState {
pub fn queued() -> Self {
Self {
status: OperationStatus::Queued,
started_at_ms: None,
finished_at_ms: None,
progress: None,
reason: None,
}
}
pub fn running() -> Self {
Self {
status: OperationStatus::Running,
started_at_ms: Some(now_ms()),
finished_at_ms: None,
progress: None,
reason: None,
}
}
pub fn completed(progress: Option<Progress>) -> Result<Self> {
if let Some(progress) = &progress {
progress.validate()?;
}
Ok(Self {
status: OperationStatus::Completed,
started_at_ms: None,
finished_at_ms: Some(now_ms()),
progress,
reason: None,
})
}
pub fn failed(reason: impl Into<String>) -> Self {
Self {
status: OperationStatus::Failed,
started_at_ms: None,
finished_at_ms: Some(now_ms()),
progress: None,
reason: Some(reason.into()),
}
}
pub fn cancelled(reason: impl Into<String>) -> Self {
Self {
status: OperationStatus::Cancelled,
started_at_ms: None,
finished_at_ms: Some(now_ms()),
progress: None,
reason: Some(reason.into()),
}
}
pub fn mark_running(&mut self) {
self.status = OperationStatus::Running;
self.started_at_ms = Some(now_ms());
self.finished_at_ms = None;
}
pub fn mark_finished(&mut self, status: OperationStatus, reason: Option<String>) {
self.status = status;
self.finished_at_ms = Some(now_ms());
self.reason = reason;
}
pub fn set_progress(&mut self, progress: Progress) -> Result<()> {
progress.validate()?;
self.progress = Some(progress);
Ok(())
}
}
#[derive(Debug, Clone)]
struct LifecycleState {
mode: Mode,
backup_state: OperationState,
restore_state: OperationState,
restore_metadata: Option<RestoreMetadata>,
}
#[derive(Debug)]
pub struct LifecycleStateManager {
inner: RwLock<LifecycleState>,
}
impl LifecycleStateManager {
pub fn new(initial_mode: Mode) -> Self {
Self {
inner: RwLock::new(LifecycleState {
mode: initial_mode,
backup_state: OperationState::queued(),
restore_state: OperationState::queued(),
restore_metadata: None,
}),
}
}
pub fn current_mode(&self) -> Mode {
self.inner
.read()
.expect("lifecycle state lock poisoned")
.mode
}
pub fn set_mode(&self, mode: Mode) {
self.inner
.write()
.expect("lifecycle state lock poisoned")
.mode = mode;
}
pub fn backup_state(&self) -> OperationState {
self.inner
.read()
.expect("lifecycle state lock poisoned")
.backup_state
.clone()
}
pub fn set_backup_state(&self, state: OperationState) {
self.inner
.write()
.expect("lifecycle state lock poisoned")
.backup_state = state;
}
pub fn restore_state(&self) -> OperationState {
self.inner
.read()
.expect("lifecycle state lock poisoned")
.restore_state
.clone()
}
pub fn set_restore_state(&self, state: OperationState) {
self.inner
.write()
.expect("lifecycle state lock poisoned")
.restore_state = state;
}
pub fn restore_metadata(&self) -> Option<RestoreMetadata> {
self.inner
.read()
.expect("lifecycle state lock poisoned")
.restore_metadata
.clone()
}
pub fn set_restore_metadata(&self, metadata: Option<RestoreMetadata>) {
self.inner
.write()
.expect("lifecycle state lock poisoned")
.restore_metadata = metadata;
}
pub fn should_block_writes(&self) -> bool {
matches!(self.current_mode(), Mode::ReadOnly | Mode::Maintenance)
}
pub fn check_write_allowed(&self) -> Result<()> {
match self.current_mode() {
Mode::Normal => Ok(()),
Mode::ReadOnly => Err(ServerError::Conflict(
"writes are blocked in read_only mode".to_string(),
)),
Mode::Maintenance => Err(ServerError::Conflict(
"writes are blocked in maintenance mode".to_string(),
)),
}
}
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn progress_validation_rejects_empty() {
let invalid = Progress {
percent: None,
bytes_processed: None,
};
assert!(invalid.validate().is_err());
let mut state = OperationState::running();
assert!(state.set_progress(invalid).is_err());
}
#[test]
fn progress_validation_accepts_percent_and_bytes() {
let percent = Progress::percent(10);
assert!(percent.validate().is_ok());
let bytes = Progress::bytes(2048);
assert!(bytes.validate().is_ok());
}
#[test]
fn lifecycle_write_guard_blocks_non_normal_modes() {
let manager = LifecycleStateManager::new(Mode::Normal);
assert_eq!(manager.current_mode(), Mode::Normal);
assert!(!manager.should_block_writes());
assert!(manager.check_write_allowed().is_ok());
manager.set_mode(Mode::ReadOnly);
assert!(manager.should_block_writes());
assert!(manager.check_write_allowed().is_err());
manager.set_mode(Mode::Maintenance);
assert!(manager.should_block_writes());
assert!(manager.check_write_allowed().is_err());
}
#[test]
fn operation_state_transitions_capture_timestamps() {
let mut state = OperationState::queued();
assert_eq!(state.status, OperationStatus::Queued);
assert!(state.started_at_ms.is_none());
assert!(state.finished_at_ms.is_none());
state.mark_running();
assert_eq!(state.status, OperationStatus::Running);
assert!(state.started_at_ms.is_some());
assert!(state.finished_at_ms.is_none());
state.mark_finished(OperationStatus::Completed, None);
assert_eq!(state.status, OperationStatus::Completed);
assert!(state.finished_at_ms.is_some());
}
#[test]
fn restore_metadata_is_stored_and_loaded() {
let manager = LifecycleStateManager::new(Mode::Normal);
let metadata = RestoreMetadata {
backup_id: "backup-1".to_string(),
location: "/tmp/backup".to_string(),
restored_at_ms: 1234,
size_bytes: 512,
};
manager.set_restore_metadata(Some(metadata.clone()));
assert_eq!(manager.restore_metadata(), Some(metadata));
manager.set_restore_metadata(None);
assert!(manager.restore_metadata().is_none());
}
}