Skip to main content

vcon_server/
datadir.rs

1//! Who may use the data directory at once. `serve` runs alone among
2//! servers; a command that rewrites the registry or the tenants' catalogs
3//! runs alone; the other commands run beside a server and beside each
4//! other, but never beside a command that runs alone.
5
6use 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/// How a process shares the data directory.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Sharing {
15    /// A server: never beside another server or a command that runs alone.
16    /// Two servers on one directory would each recover its tenants at open,
17    /// and one's recovery could remove a write the other has under way.
18    Serve,
19    /// A command that runs with nothing else in the data directory.
20    Alone,
21    /// A command that runs beside a server and other such commands.
22    Beside,
23}
24
25/// The work a process does in the data directory.
26#[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    /// How this work shares the data directory.
48    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/// The data directory's locks, held while this value lives: the open files
71/// are kept only for their locks.
72#[derive(Debug)]
73pub struct DataDirLock {
74    _held: Vec<File>,
75}
76
77/// Why the data directory could not be taken.
78#[derive(Debug)]
79pub enum LockError {
80    /// Something that may not run beside this `Sharing` holds it.
81    InUse(Sharing),
82    /// A lock file could not be opened or locked.
83    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
103/// Takes the data directory for work that shares it as `sharing`, or says
104/// what holds it.
105///
106/// Two lock files carry the rule: `lock`, taken exclusively by a server and
107/// by a command that runs alone, and `lock.commands`, shared by commands
108/// that run beside a server and taken exclusively by one that runs alone.
109pub 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
130/// Opens `path`, made if missing, and locks it without waiting.
131fn 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    // SAFETY: the descriptor is this open file's, and flock takes only it
139    // and flags.
140    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    /// Each kind of work and how it shares the data directory, as the
151    /// command-line page documents it: the server alone with others beside
152    /// it, the commands that change tenants or keys under them alone, and
153    /// the rest beside a server. The match has no catch-all arm, so a new
154    /// kind of work fails to compile here until its sharing is written down.
155    #[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    /// Each pair: what holds the directory, what asks for it next, and
198    /// whether that is allowed.
199    #[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    /// A refused command that runs alone leaves nothing held: a server can
229    /// still start once the beside command ends.
230    #[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}