cargo_reclaim/background/service/
model.rs1use 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}