]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/jobstate.rs
update to proxmox-sys 0.2 crate
[proxmox-backup.git] / src / server / jobstate.rs
CommitLineData
e6263c26
DC
1//! Generic JobState handling
2//!
3//! A 'Job' can have 3 states
4//! - Created, when a schedule was created but never executed
5//! - Started, when a job is running right now
6//! - Finished, when a job was running in the past
7//!
8//! and is identified by 2 values: jobtype and jobname (e.g. 'syncjob' and 'myfirstsyncjob')
9//!
10//! This module Provides 2 helper structs to handle those coniditons
11//! 'Job' which handles locking and writing to a file
12//! 'JobState' which is the actual state
13//!
14//! an example usage would be
15//! ```no_run
16//! # use anyhow::{bail, Error};
b9700a9f 17//! # use proxmox_rest_server::TaskState;
d642802d 18//! # use proxmox_backup::server::jobstate::*;
77bd2a46 19//! # fn some_code() -> TaskState { TaskState::OK { endtime: 0 } }
e6263c26
DC
20//! # fn code() -> Result<(), Error> {
21//! // locks the correct file under /var/lib
22//! // or fails if someone else holds the lock
23//! let mut job = match Job::new("jobtype", "jobname") {
24//! Ok(job) => job,
25//! Err(err) => bail!("could not lock jobstate"),
26//! };
27//!
93bb51fe 28//! // job holds the lock, we can start it
e6263c26
DC
29//! job.start("someupid")?;
30//! // do something
31//! let task_state = some_code();
32//! job.finish(task_state)?;
33//!
34//! // release the lock
35//! drop(job);
36//! # Ok(())
37//! # }
38//!
39//! ```
e6263c26 40use std::path::{Path, PathBuf};
e6263c26 41
4ea831bf 42use anyhow::{bail, format_err, Error};
dd2162f6
WB
43use serde::{Deserialize, Serialize};
44
25877d05 45use proxmox_sys::fs::{
7526d864 46 create_path, file_read_optional_string, replace_file, CreateOptions,
4ea831bf 47};
dd2162f6 48
15cc41b6 49use proxmox_time::{compute_next_event, parse_calendar_event};
b9352095
DM
50
51use pbs_buildcfg::PROXMOX_BACKUP_STATE_DIR_M;
21211748 52use pbs_config::{open_backup_lockfile, BackupLockGuard};
6227654a 53use pbs_api_types::{UPID, JobScheduleStatus};
e6263c26 54
b9700a9f 55use proxmox_rest_server::{upid_read_status, worker_is_active_local, TaskState};
e6263c26 56
4ea831bf 57#[derive(Serialize, Deserialize)]
3e3b505c 58#[serde(rename_all = "kebab-case")]
e6263c26
DC
59/// Represents the State of a specific Job
60pub enum JobState {
61 /// A job was created at 'time', but never started/finished
62 Created { time: i64 },
63 /// The Job was last started in 'upid',
64 Started { upid: String },
951fe0cb
DC
65 /// The Job was last started in 'upid', which finished with 'state', and was last updated at 'updated'
66 Finished {
67 upid: String,
68 state: TaskState,
69 updated: Option<i64>,
70 },
e6263c26
DC
71}
72
73/// Represents a Job and holds the correct lock
74pub struct Job {
75 jobtype: String,
76 jobname: String,
77 /// The State of the job
78 pub state: JobState,
7526d864 79 _lock: BackupLockGuard,
e6263c26
DC
80}
81
b9352095 82const JOB_STATE_BASEDIR: &str = concat!(PROXMOX_BACKUP_STATE_DIR_M!(), "/jobstates");
e6263c26
DC
83
84/// Create jobstate stat dir with correct permission
85pub fn create_jobstate_dir() -> Result<(), Error> {
21211748 86 let backup_user = pbs_config::backup_user()?;
b9352095 87
e6263c26
DC
88 let opts = CreateOptions::new()
89 .owner(backup_user.uid)
90 .group(backup_user.gid);
91
b9352095 92 create_path(JOB_STATE_BASEDIR, Some(opts.clone()), Some(opts))
e6263c26
DC
93 .map_err(|err: Error| format_err!("unable to create rrdb stat dir - {}", err))?;
94
95 Ok(())
96}
97
98fn get_path(jobtype: &str, jobname: &str) -> PathBuf {
99 let mut path = PathBuf::from(JOB_STATE_BASEDIR);
100 path.push(format!("{}-{}.json", jobtype, jobname));
101 path
102}
103
7526d864 104fn get_lock<P>(path: P) -> Result<BackupLockGuard, Error>
e6263c26 105where
4ea831bf 106 P: AsRef<Path>,
e6263c26
DC
107{
108 let mut path = path.as_ref().to_path_buf();
109 path.set_extension("lck");
7526d864 110 open_backup_lockfile(&path, None, true)
e6263c26
DC
111}
112
113/// Removes the statefile of a job, this is useful if we delete a job
114pub fn remove_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
93bb51fe 115 let mut path = get_path(jobtype, jobname);
e6263c26 116 let _lock = get_lock(&path)?;
4ea831bf
WB
117 std::fs::remove_file(&path).map_err(|err| {
118 format_err!(
119 "cannot remove statefile for {} - {}: {}",
120 jobtype,
121 jobname,
122 err
123 )
124 })?;
93bb51fe
DC
125 path.set_extension("lck");
126 // ignore errors
4ea831bf
WB
127 let _ = std::fs::remove_file(&path).map_err(|err| {
128 format_err!(
129 "cannot remove lockfile for {} - {}: {}",
130 jobtype,
131 jobname,
132 err
133 )
134 });
93bb51fe
DC
135 Ok(())
136}
137
138/// Creates the statefile with the state 'Created'
139/// overwrites if it exists already
140pub fn create_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
141 let mut job = Job::new(jobtype, jobname)?;
142 job.write_state()
e6263c26
DC
143}
144
951fe0cb 145/// Tries to update the state file with the current time
37a634f5
DC
146/// if the job is currently running, does nothing.
147/// Intended for use when the schedule changes.
148pub fn update_job_last_run_time(jobtype: &str, jobname: &str) -> Result<(), Error> {
951fe0cb
DC
149 let mut job = match Job::new(jobtype, jobname) {
150 Ok(job) => job,
151 Err(_) => return Ok(()), // was locked (running), so do not update
152 };
6ef1b649 153 let time = proxmox_time::epoch_i64();
951fe0cb
DC
154
155 job.state = match JobState::load(jobtype, jobname)? {
156 JobState::Created { .. } => JobState::Created { time },
157 JobState::Started { .. } => return Ok(()), // currently running (without lock?)
158 JobState::Finished {
159 upid,
160 state,
161 updated: _,
162 } => JobState::Finished {
163 upid,
164 state,
165 updated: Some(time),
166 },
167 };
168 job.write_state()
169}
170
e6263c26
DC
171/// Returns the last run time of a job by reading the statefile
172/// Note that this is not locked
173pub fn last_run_time(jobtype: &str, jobname: &str) -> Result<i64, Error> {
174 match JobState::load(jobtype, jobname)? {
175 JobState::Created { time } => Ok(time),
951fe0cb
DC
176 JobState::Finished {
177 updated: Some(time),
178 ..
179 } => Ok(time),
180 JobState::Started { upid }
181 | JobState::Finished {
182 upid,
183 state: _,
184 updated: None,
185 } => {
4ea831bf
WB
186 let upid: UPID = upid
187 .parse()
188 .map_err(|err| format_err!("could not parse upid from state: {}", err))?;
e6263c26
DC
189 Ok(upid.starttime)
190 }
191 }
192}
193
194impl JobState {
195 /// Loads and deserializes the jobstate from type and name.
196 /// When the loaded state indicates a started UPID,
197 /// we go and check if it has already stopped, and
198 /// returning the correct state.
199 ///
200 /// This does not update the state in the file.
201 pub fn load(jobtype: &str, jobname: &str) -> Result<Self, Error> {
202 if let Some(state) = file_read_optional_string(get_path(jobtype, jobname))? {
203 match serde_json::from_str(&state)? {
204 JobState::Started { upid } => {
4ea831bf
WB
205 let parsed: UPID = upid
206 .parse()
e6263c26
DC
207 .map_err(|err| format_err!("error parsing upid: {}", err))?;
208
209 if !worker_is_active_local(&parsed) {
77bd2a46 210 let state = upid_read_status(&parsed)
e6263c26
DC
211 .map_err(|err| format_err!("error reading upid log status: {}", err))?;
212
951fe0cb
DC
213 Ok(JobState::Finished {
214 upid,
215 state,
216 updated: None,
217 })
e6263c26
DC
218 } else {
219 Ok(JobState::Started { upid })
220 }
221 }
222 other => Ok(other),
223 }
224 } else {
225 Ok(JobState::Created {
6ef1b649 226 time: proxmox_time::epoch_i64() - 30,
e6263c26
DC
227 })
228 }
229 }
230}
231
232impl Job {
233 /// Creates a new instance of a job with the correct lock held
234 /// (will be hold until the job is dropped again).
235 ///
236 /// This does not load the state from the file, to do that,
237 /// 'load' must be called
238 pub fn new(jobtype: &str, jobname: &str) -> Result<Self, Error> {
239 let path = get_path(jobtype, jobname);
240
241 let _lock = get_lock(&path)?;
242
4ea831bf 243 Ok(Self {
e6263c26
DC
244 jobtype: jobtype.to_string(),
245 jobname: jobname.to_string(),
246 state: JobState::Created {
6ef1b649 247 time: proxmox_time::epoch_i64(),
e6263c26
DC
248 },
249 _lock,
250 })
251 }
252
e6263c26
DC
253 /// Start the job and update the statefile accordingly
254 /// Fails if the job was already started
255 pub fn start(&mut self, upid: &str) -> Result<(), Error> {
b92cad09
FG
256 if let JobState::Started { .. } = self.state {
257 bail!("cannot start job that is started!");
e6263c26
DC
258 }
259
4ea831bf 260 self.state = JobState::Started {
e6263c26
DC
261 upid: upid.to_string(),
262 };
263
264 self.write_state()
265 }
266
267 /// Finish the job and update the statefile accordingly with the given taskstate
268 /// Fails if the job was not yet started
269 pub fn finish(&mut self, state: TaskState) -> Result<(), Error> {
270 let upid = match &self.state {
271 JobState::Created { .. } => bail!("cannot finish when not started"),
272 JobState::Started { upid } => upid,
273 JobState::Finished { upid, .. } => upid,
4ea831bf
WB
274 }
275 .to_string();
e6263c26 276
951fe0cb
DC
277 self.state = JobState::Finished {
278 upid,
279 state,
280 updated: None,
281 };
e6263c26
DC
282
283 self.write_state()
284 }
285
713b66b6
DC
286 pub fn jobtype(&self) -> &str {
287 &self.jobtype
288 }
289
290 pub fn jobname(&self) -> &str {
291 &self.jobname
292 }
293
e6263c26
DC
294 fn write_state(&mut self) -> Result<(), Error> {
295 let serialized = serde_json::to_string(&self.state)?;
296 let path = get_path(&self.jobtype, &self.jobname);
297
21211748 298 let backup_user = pbs_config::backup_user()?;
e6263c26
DC
299 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
300 // set the correct owner/group/permissions while saving file
301 // owner(rw) = backup, group(r)= backup
302 let options = CreateOptions::new()
303 .perm(mode)
304 .owner(backup_user.uid)
305 .group(backup_user.gid);
306
e0a19d33 307 replace_file(path, serialized.as_bytes(), options, false)
e6263c26
DC
308 }
309}
18602085
DM
310
311pub fn compute_schedule_status(
312 job_state: &JobState,
313 schedule: Option<&str>,
314) -> Result<JobScheduleStatus, Error> {
951fe0cb 315 let (upid, endtime, state, last) = match job_state {
18602085
DM
316 JobState::Created { time } => (None, None, None, *time),
317 JobState::Started { upid } => {
318 let parsed_upid: UPID = upid.parse()?;
319 (Some(upid), None, None, parsed_upid.starttime)
951fe0cb
DC
320 }
321 JobState::Finished {
322 upid,
323 state,
324 updated,
325 } => {
326 let last = updated.unwrap_or_else(|| state.endtime());
327 (
328 Some(upid),
329 Some(state.endtime()),
330 Some(state.to_string()),
331 last,
332 )
333 }
18602085
DM
334 };
335
336 let mut status = JobScheduleStatus::default();
337 status.last_run_upid = upid.map(String::from);
338 status.last_run_state = state;
339 status.last_run_endtime = endtime;
340
18602085 341 if let Some(schedule) = schedule {
951fe0cb 342 if let Ok(event) = parse_calendar_event(&schedule) {
18602085
DM
343 // ignore errors
344 status.next_run = compute_next_event(&event, last, false).unwrap_or(None);
345 }
346 }
347
348 Ok(status)
349}