Skip to main content

cargo_reclaim/background/service/
model.rs

1use std::error::Error;
2use std::fmt;
3use std::path::PathBuf;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::ReclaimError;
9use crate::persistence::{PersistedTimestamp, PlanPersistenceError};
10use crate::scheduler::{SchedulerError, SchedulerMode};
11
12use super::super::BackgroundRunnerError;
13
14pub const BACKGROUND_SERVICE_STATE_SCHEMA_VERSION: u16 = 1;
15pub const DEFAULT_BACKGROUND_CHECK_EVERY: Duration = Duration::from_secs(60 * 60);
16
17pub type BackgroundServiceResult<T> = Result<T, BackgroundServiceError>;
18
19#[derive(Debug)]
20pub enum BackgroundServiceError {
21    Io {
22        path: PathBuf,
23        source: std::io::Error,
24    },
25    Serialize {
26        path: PathBuf,
27        source: serde_json::Error,
28    },
29    Json {
30        path: PathBuf,
31        source: serde_json::Error,
32    },
33    Timestamp(PlanPersistenceError),
34    Scheduler(SchedulerError),
35    Runner(BackgroundRunnerError),
36    Config(String),
37    AlreadyRunning {
38        lock_path: PathBuf,
39    },
40    StaleLock {
41        lock_path: PathBuf,
42    },
43}
44
45impl fmt::Display for BackgroundServiceError {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
49            Self::Serialize { path, source } => {
50                write!(
51                    formatter,
52                    "failed to serialize {}: {source}",
53                    path.display()
54                )
55            }
56            Self::Json { path, source } => {
57                write!(formatter, "failed to parse {}: {source}", path.display())
58            }
59            Self::Timestamp(source) => write!(formatter, "invalid service timestamp: {source}"),
60            Self::Scheduler(source) => source.fmt(formatter),
61            Self::Runner(source) => source.fmt(formatter),
62            Self::Config(message) => formatter.write_str(message),
63            Self::AlreadyRunning { lock_path } => write!(
64                formatter,
65                "cargo-reclaim scheduler service is already running; lock exists at {}",
66                lock_path.display()
67            ),
68            Self::StaleLock { lock_path } => write!(
69                formatter,
70                "cargo-reclaim scheduler service lock exists at {}; stale-lock recovery is not automatic yet",
71                lock_path.display()
72            ),
73        }
74    }
75}
76
77impl Error for BackgroundServiceError {
78    fn source(&self) -> Option<&(dyn Error + 'static)> {
79        match self {
80            Self::Io { source, .. } => Some(source),
81            Self::Serialize { source, .. } | Self::Json { source, .. } => Some(source),
82            Self::Timestamp(source) => Some(source),
83            Self::Scheduler(source) => Some(source),
84            Self::Runner(source) => Some(source),
85            Self::Config(_) | Self::AlreadyRunning { .. } | Self::StaleLock { .. } => None,
86        }
87    }
88}
89
90impl From<SchedulerError> for BackgroundServiceError {
91    fn from(error: SchedulerError) -> Self {
92        Self::Scheduler(error)
93    }
94}
95
96impl From<BackgroundRunnerError> for BackgroundServiceError {
97    fn from(error: BackgroundRunnerError) -> Self {
98        Self::Runner(error)
99    }
100}
101
102impl From<ReclaimError> for BackgroundServiceError {
103    fn from(error: ReclaimError) -> Self {
104        Self::Config(error.to_string())
105    }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct BackgroundServiceOptions {
110    pub config_path: PathBuf,
111    pub state_dir: PathBuf,
112    pub log_dir: PathBuf,
113    pub mode: Option<SchedulerMode>,
114    pub max_cycles: Option<usize>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct BackgroundServicePaths {
119    pub state_dir: PathBuf,
120    pub log_dir: PathBuf,
121    pub plans_dir: PathBuf,
122    pub lock_path: PathBuf,
123    pub state_path: PathBuf,
124    pub runs_log_path: PathBuf,
125}
126
127impl BackgroundServicePaths {
128    pub fn new(state_dir: impl Into<PathBuf>, log_dir: impl Into<PathBuf>) -> Self {
129        let state_dir = state_dir.into();
130        let log_dir = log_dir.into();
131        Self {
132            plans_dir: state_dir.join("plans"),
133            lock_path: state_dir.join("service.lock"),
134            state_path: state_dir.join("service-state.json"),
135            runs_log_path: log_dir.join("runs.jsonl"),
136            state_dir,
137            log_dir,
138        }
139    }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct BackgroundServiceState {
144    pub schema_version: u16,
145    pub status: BackgroundServiceStatus,
146    pub pid: Option<u32>,
147    pub started_at: Option<PersistedTimestamp>,
148    pub last_run_id: Option<String>,
149    pub last_run_at: Option<PersistedTimestamp>,
150    pub next_run_at: Option<PersistedTimestamp>,
151    pub consecutive_failures: u32,
152    pub last_problem: Option<String>,
153}
154
155impl BackgroundServiceState {
156    pub fn missing() -> Self {
157        Self {
158            schema_version: BACKGROUND_SERVICE_STATE_SCHEMA_VERSION,
159            status: BackgroundServiceStatus::Unknown,
160            pid: None,
161            started_at: None,
162            last_run_id: None,
163            last_run_at: None,
164            next_run_at: None,
165            consecutive_failures: 0,
166            last_problem: Some("service state file does not exist".to_owned()),
167        }
168    }
169
170    pub(crate) fn running(started_at: PersistedTimestamp) -> Self {
171        Self {
172            schema_version: BACKGROUND_SERVICE_STATE_SCHEMA_VERSION,
173            status: BackgroundServiceStatus::Running,
174            pid: Some(std::process::id()),
175            started_at: Some(started_at),
176            last_run_id: None,
177            last_run_at: None,
178            next_run_at: None,
179            consecutive_failures: 0,
180            last_problem: None,
181        }
182    }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub enum BackgroundServiceStatus {
188    Running,
189    Stopped,
190    Unknown,
191    Stale,
192    Error,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct BackgroundServiceRunSummary {
197    pub state: BackgroundServiceState,
198    pub cycles_completed: usize,
199}