]> git.proxmox.com Git - proxmox-backup.git/blame - src/config/jobstate.rs
log rotate: do NOT overwrite file with possible writers
[proxmox-backup.git] / src / config / 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};
17//! # use proxmox_backup::server::TaskState;
18//! # use proxmox_backup::config::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//! ```
40use std::fs::File;
41use std::path::{Path, PathBuf};
42use std::time::Duration;
43
4ea831bf
WB
44use anyhow::{bail, format_err, Error};
45use proxmox::tools::fs::{
46 create_path, file_read_optional_string, open_file_locked, replace_file, CreateOptions,
47};
48use serde::{Deserialize, Serialize};
e6263c26 49
4ea831bf 50use crate::server::{upid_read_status, worker_is_active_local, TaskState, UPID};
e6263c26 51
4ea831bf
WB
52#[serde(rename_all = "kebab-case")]
53#[derive(Serialize, Deserialize)]
e6263c26
DC
54/// Represents the State of a specific Job
55pub enum JobState {
56 /// A job was created at 'time', but never started/finished
57 Created { time: i64 },
58 /// The Job was last started in 'upid',
59 Started { upid: String },
77bd2a46 60 /// The Job was last started in 'upid', which finished with 'state'
4ea831bf 61 Finished { upid: String, state: TaskState },
e6263c26
DC
62}
63
64/// Represents a Job and holds the correct lock
65pub struct Job {
66 jobtype: String,
67 jobname: String,
68 /// The State of the job
69 pub state: JobState,
70 _lock: File,
71}
72
73const JOB_STATE_BASEDIR: &str = "/var/lib/proxmox-backup/jobstates";
74
75/// Create jobstate stat dir with correct permission
76pub fn create_jobstate_dir() -> Result<(), Error> {
77 let backup_user = crate::backup::backup_user()?;
78 let opts = CreateOptions::new()
79 .owner(backup_user.uid)
80 .group(backup_user.gid);
81
82 create_path(JOB_STATE_BASEDIR, None, Some(opts))
83 .map_err(|err: Error| format_err!("unable to create rrdb stat dir - {}", err))?;
84
85 Ok(())
86}
87
88fn get_path(jobtype: &str, jobname: &str) -> PathBuf {
89 let mut path = PathBuf::from(JOB_STATE_BASEDIR);
90 path.push(format!("{}-{}.json", jobtype, jobname));
91 path
92}
93
94fn get_lock<P>(path: P) -> Result<File, Error>
95where
4ea831bf 96 P: AsRef<Path>,
e6263c26
DC
97{
98 let mut path = path.as_ref().to_path_buf();
99 path.set_extension("lck");
b56c111e 100 let lock = open_file_locked(&path, Duration::new(10, 0), true)?;
93bb51fe
DC
101 let backup_user = crate::backup::backup_user()?;
102 nix::unistd::chown(&path, Some(backup_user.uid), Some(backup_user.gid))?;
103 Ok(lock)
e6263c26
DC
104}
105
106/// Removes the statefile of a job, this is useful if we delete a job
107pub fn remove_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
93bb51fe 108 let mut path = get_path(jobtype, jobname);
e6263c26 109 let _lock = get_lock(&path)?;
4ea831bf
WB
110 std::fs::remove_file(&path).map_err(|err| {
111 format_err!(
112 "cannot remove statefile for {} - {}: {}",
113 jobtype,
114 jobname,
115 err
116 )
117 })?;
93bb51fe
DC
118 path.set_extension("lck");
119 // ignore errors
4ea831bf
WB
120 let _ = std::fs::remove_file(&path).map_err(|err| {
121 format_err!(
122 "cannot remove lockfile for {} - {}: {}",
123 jobtype,
124 jobname,
125 err
126 )
127 });
93bb51fe
DC
128 Ok(())
129}
130
131/// Creates the statefile with the state 'Created'
132/// overwrites if it exists already
133pub fn create_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
134 let mut job = Job::new(jobtype, jobname)?;
135 job.write_state()
e6263c26
DC
136}
137
138/// Returns the last run time of a job by reading the statefile
139/// Note that this is not locked
140pub fn last_run_time(jobtype: &str, jobname: &str) -> Result<i64, Error> {
141 match JobState::load(jobtype, jobname)? {
142 JobState::Created { time } => Ok(time),
143 JobState::Started { upid } | JobState::Finished { upid, .. } => {
4ea831bf
WB
144 let upid: UPID = upid
145 .parse()
146 .map_err(|err| format_err!("could not parse upid from state: {}", err))?;
e6263c26
DC
147 Ok(upid.starttime)
148 }
149 }
150}
151
152impl JobState {
153 /// Loads and deserializes the jobstate from type and name.
154 /// When the loaded state indicates a started UPID,
155 /// we go and check if it has already stopped, and
156 /// returning the correct state.
157 ///
158 /// This does not update the state in the file.
159 pub fn load(jobtype: &str, jobname: &str) -> Result<Self, Error> {
160 if let Some(state) = file_read_optional_string(get_path(jobtype, jobname))? {
161 match serde_json::from_str(&state)? {
162 JobState::Started { upid } => {
4ea831bf
WB
163 let parsed: UPID = upid
164 .parse()
e6263c26
DC
165 .map_err(|err| format_err!("error parsing upid: {}", err))?;
166
167 if !worker_is_active_local(&parsed) {
77bd2a46 168 let state = upid_read_status(&parsed)
e6263c26
DC
169 .map_err(|err| format_err!("error reading upid log status: {}", err))?;
170
4ea831bf 171 Ok(JobState::Finished { upid, state })
e6263c26
DC
172 } else {
173 Ok(JobState::Started { upid })
174 }
175 }
176 other => Ok(other),
177 }
178 } else {
179 Ok(JobState::Created {
6a7be83e 180 time: proxmox::tools::time::epoch_i64() - 30,
e6263c26
DC
181 })
182 }
183 }
184}
185
186impl Job {
187 /// Creates a new instance of a job with the correct lock held
188 /// (will be hold until the job is dropped again).
189 ///
190 /// This does not load the state from the file, to do that,
191 /// 'load' must be called
192 pub fn new(jobtype: &str, jobname: &str) -> Result<Self, Error> {
193 let path = get_path(jobtype, jobname);
194
195 let _lock = get_lock(&path)?;
196
4ea831bf 197 Ok(Self {
e6263c26
DC
198 jobtype: jobtype.to_string(),
199 jobname: jobname.to_string(),
200 state: JobState::Created {
6a7be83e 201 time: proxmox::tools::time::epoch_i64(),
e6263c26
DC
202 },
203 _lock,
204 })
205 }
206
e6263c26
DC
207 /// Start the job and update the statefile accordingly
208 /// Fails if the job was already started
209 pub fn start(&mut self, upid: &str) -> Result<(), Error> {
210 match self.state {
211 JobState::Started { .. } => {
212 bail!("cannot start job that is started!");
213 }
214 _ => {}
215 }
216
4ea831bf 217 self.state = JobState::Started {
e6263c26
DC
218 upid: upid.to_string(),
219 };
220
221 self.write_state()
222 }
223
224 /// Finish the job and update the statefile accordingly with the given taskstate
225 /// Fails if the job was not yet started
226 pub fn finish(&mut self, state: TaskState) -> Result<(), Error> {
227 let upid = match &self.state {
228 JobState::Created { .. } => bail!("cannot finish when not started"),
229 JobState::Started { upid } => upid,
230 JobState::Finished { upid, .. } => upid,
4ea831bf
WB
231 }
232 .to_string();
e6263c26 233
4ea831bf 234 self.state = JobState::Finished { upid, state };
e6263c26
DC
235
236 self.write_state()
237 }
238
713b66b6
DC
239 pub fn jobtype(&self) -> &str {
240 &self.jobtype
241 }
242
243 pub fn jobname(&self) -> &str {
244 &self.jobname
245 }
246
e6263c26
DC
247 fn write_state(&mut self) -> Result<(), Error> {
248 let serialized = serde_json::to_string(&self.state)?;
249 let path = get_path(&self.jobtype, &self.jobname);
250
251 let backup_user = crate::backup::backup_user()?;
252 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
253 // set the correct owner/group/permissions while saving file
254 // owner(rw) = backup, group(r)= backup
255 let options = CreateOptions::new()
256 .perm(mode)
257 .owner(backup_user.uid)
258 .group(backup_user.gid);
259
4ea831bf 260 replace_file(path, serialized.as_bytes(), options)
e6263c26
DC
261 }
262}