1use std::fs::{File, OpenOptions};
7use std::os::fd::AsRawFd;
8use std::os::unix::fs::OpenOptionsExt;
9use std::path::Path;
10use std::{fmt, io};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Sharing {
15 Serve,
19 Alone,
21 Beside,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Work {
28 Serve,
29 TenantCreate,
30 TenantList,
31 TenantSetRetention,
32 TenantRemove,
33 TenantSetMetadata,
34 ExportParquet,
35 KeyCreate,
36 KeyList,
37 KeyRevoke,
38 RekeyTenant,
39 RekeyMaster,
40 AuditExport,
41 Check,
42 Backup,
43 StorageMoveObjects,
44}
45
46impl Work {
47 pub fn sharing(self) -> Sharing {
49 match self {
50 Work::Serve => Sharing::Serve,
51 Work::TenantCreate
52 | Work::TenantRemove
53 | Work::RekeyTenant
54 | Work::RekeyMaster
55 | Work::Check
56 | Work::StorageMoveObjects => Sharing::Alone,
57 Work::TenantList
58 | Work::TenantSetRetention
59 | Work::KeyCreate
60 | Work::KeyList
61 | Work::KeyRevoke
62 | Work::AuditExport
63 | Work::Backup
64 | Work::TenantSetMetadata
65 | Work::ExportParquet => Sharing::Beside,
66 }
67 }
68}
69
70#[derive(Debug)]
73pub struct DataDirLock {
74 _held: Vec<File>,
75}
76
77#[derive(Debug)]
79pub enum LockError {
80 InUse(Sharing),
82 Io(io::Error),
84}
85
86impl fmt::Display for LockError {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 LockError::InUse(Sharing::Serve) => f.write_str(
90 "the data directory is in use by another vcon-server or a command that runs alone",
91 ),
92 LockError::InUse(Sharing::Alone) => f.write_str(
93 "the data directory is in use; stop the server and any other command first",
94 ),
95 LockError::InUse(Sharing::Beside) => {
96 f.write_str("the data directory is in use by a command that runs alone")
97 }
98 LockError::Io(e) => write!(f, "the data directory cannot be locked: {e}"),
99 }
100 }
101}
102
103pub fn lock(dir: &Path, sharing: Sharing) -> Result<DataDirLock, LockError> {
110 let take = |name: &str, how| {
111 flock(&dir.join(name), how).map_err(|e| match e.kind() {
112 io::ErrorKind::WouldBlock => LockError::InUse(sharing),
113 _ => LockError::Io(e),
114 })
115 };
116 let held = match sharing {
117 Sharing::Serve => vec![take(SERVERS, libc::LOCK_EX)?],
118 Sharing::Alone => vec![
119 take(SERVERS, libc::LOCK_EX)?,
120 take(COMMANDS, libc::LOCK_EX)?,
121 ],
122 Sharing::Beside => vec![take(COMMANDS, libc::LOCK_SH)?],
123 };
124 Ok(DataDirLock { _held: held })
125}
126
127const SERVERS: &str = "lock";
128const COMMANDS: &str = "lock.commands";
129
130fn flock(path: &Path, how: libc::c_int) -> io::Result<File> {
132 let file = OpenOptions::new()
133 .create(true)
134 .truncate(false)
135 .write(true)
136 .mode(crate::fs::FILE_MODE)
137 .open(path)?;
138 if unsafe { libc::flock(file.as_raw_fd(), how | libc::LOCK_NB) } != 0 {
141 return Err(io::Error::last_os_error());
142 }
143 Ok(file)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
156 fn every_kind_of_work_shares_the_directory_as_documented() {
157 let documented = |work: Work| match work {
158 Work::Serve => Sharing::Serve,
159 Work::TenantCreate => Sharing::Alone,
160 Work::TenantList => Sharing::Beside,
161 Work::TenantSetRetention => Sharing::Beside,
162 Work::TenantRemove => Sharing::Alone,
163 Work::KeyCreate => Sharing::Beside,
164 Work::KeyList => Sharing::Beside,
165 Work::KeyRevoke => Sharing::Beside,
166 Work::RekeyTenant => Sharing::Alone,
167 Work::RekeyMaster => Sharing::Alone,
168 Work::AuditExport => Sharing::Beside,
169 Work::Check => Sharing::Alone,
170 Work::Backup => Sharing::Beside,
171 Work::TenantSetMetadata => Sharing::Beside,
172 Work::ExportParquet => Sharing::Beside,
173 Work::StorageMoveObjects => Sharing::Alone,
174 };
175 for work in [
176 Work::Serve,
177 Work::TenantCreate,
178 Work::TenantList,
179 Work::TenantSetRetention,
180 Work::TenantRemove,
181 Work::KeyCreate,
182 Work::KeyList,
183 Work::KeyRevoke,
184 Work::RekeyTenant,
185 Work::RekeyMaster,
186 Work::AuditExport,
187 Work::Check,
188 Work::Backup,
189 Work::TenantSetMetadata,
190 Work::ExportParquet,
191 Work::StorageMoveObjects,
192 ] {
193 assert_eq!(work.sharing(), documented(work), "{work:?}");
194 }
195 }
196
197 #[test]
200 fn who_may_run_beside_whom() {
201 use Sharing::{Alone, Beside, Serve};
202 for (first, second, allowed) in [
203 (Serve, Serve, false),
204 (Serve, Alone, false),
205 (Serve, Beside, true),
206 (Alone, Serve, false),
207 (Alone, Alone, false),
208 (Alone, Beside, false),
209 (Beside, Serve, true),
210 (Beside, Alone, false),
211 (Beside, Beside, true),
212 ] {
213 let dir = tempfile::tempdir().expect("temp dir");
214 let held = lock(dir.path(), first).expect("the first lock");
215 match lock(dir.path(), second) {
216 Ok(_) => assert!(allowed, "{second:?} beside {first:?}"),
217 Err(LockError::InUse(asked)) => {
218 assert!(!allowed, "{second:?} refused beside {first:?}");
219 assert_eq!(asked, second);
220 }
221 Err(e) => panic!("{first:?} then {second:?}: {e}"),
222 }
223 drop(held);
224 assert!(lock(dir.path(), second).is_ok(), "{second:?} once free");
225 }
226 }
227
228 #[test]
231 fn a_refused_lock_holds_nothing() {
232 let dir = tempfile::tempdir().expect("temp dir");
233 let beside = lock(dir.path(), Sharing::Beside).expect("beside");
234 assert!(lock(dir.path(), Sharing::Alone).is_err());
235 let server = lock(dir.path(), Sharing::Serve).expect("serve beside");
236 drop((beside, server));
237 assert!(lock(dir.path(), Sharing::Alone).is_ok());
238 }
239
240 #[test]
241 fn each_refusal_says_what_holds_the_directory() {
242 for (sharing, words) in [
243 (Sharing::Serve, "another vcon-server or a command"),
244 (Sharing::Alone, "stop the server"),
245 (Sharing::Beside, "runs alone"),
246 ] {
247 let text = LockError::InUse(sharing).to_string();
248 assert!(text.contains(words), "{sharing:?}: {text}");
249 }
250 }
251
252 #[test]
253 fn a_data_directory_that_cannot_hold_a_lock_file_is_an_error() {
254 let missing = Path::new("/nonexistent/vcon-server-data");
255 for sharing in [Sharing::Serve, Sharing::Alone, Sharing::Beside] {
256 assert!(matches!(lock(missing, sharing), Err(LockError::Io(_))));
257 }
258 }
259}