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::fmt;
7use std::fs::{File, OpenOptions};
8use std::io;
9use std::os::fd::AsRawFd;
10use std::path::Path;
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    KeyCreate,
34    KeyList,
35    KeyRevoke,
36    RekeyTenant,
37    RekeyMaster,
38    AuditExport,
39    Check,
40}
41
42impl Work {
43    /// How this work shares the data directory.
44    pub fn sharing(self) -> Sharing {
45        match self {
46            Work::Serve => Sharing::Serve,
47            Work::TenantCreate
48            | Work::TenantRemove
49            | Work::RekeyTenant
50            | Work::RekeyMaster
51            | Work::Check => Sharing::Alone,
52            Work::TenantList
53            | Work::TenantSetRetention
54            | Work::KeyCreate
55            | Work::KeyList
56            | Work::KeyRevoke
57            | Work::AuditExport => Sharing::Beside,
58        }
59    }
60}
61
62/// The data directory's locks, held while this value lives: the open files
63/// are kept only for their locks.
64#[derive(Debug)]
65pub struct DataDirLock {
66    _held: Vec<File>,
67}
68
69/// Why the data directory could not be taken.
70#[derive(Debug)]
71pub enum LockError {
72    /// Something that may not run beside this `Sharing` holds it.
73    InUse(Sharing),
74    /// A lock file could not be opened or locked.
75    Io(io::Error),
76}
77
78impl fmt::Display for LockError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            LockError::InUse(Sharing::Serve) => f.write_str(
82                "the data directory is in use by another vcon-server or a command that runs alone",
83            ),
84            LockError::InUse(Sharing::Alone) => f.write_str(
85                "the data directory is in use; stop the server and any other command first",
86            ),
87            LockError::InUse(Sharing::Beside) => {
88                f.write_str("the data directory is in use by a command that runs alone")
89            }
90            LockError::Io(e) => write!(f, "the data directory cannot be locked: {e}"),
91        }
92    }
93}
94
95/// Takes the data directory for work that shares it as `sharing`, or says
96/// what holds it.
97///
98/// Two lock files carry the rule: `lock`, taken exclusively by a server and
99/// by a command that runs alone, and `lock.commands`, shared by commands
100/// that run beside a server and taken exclusively by one that runs alone.
101pub fn lock(dir: &Path, sharing: Sharing) -> Result<DataDirLock, LockError> {
102    let take = |name: &str, how| {
103        flock(&dir.join(name), how).map_err(|e| match e.kind() {
104            io::ErrorKind::WouldBlock => LockError::InUse(sharing),
105            _ => LockError::Io(e),
106        })
107    };
108    let held = match sharing {
109        Sharing::Serve => vec![take(SERVERS, libc::LOCK_EX)?],
110        Sharing::Alone => vec![
111            take(SERVERS, libc::LOCK_EX)?,
112            take(COMMANDS, libc::LOCK_EX)?,
113        ],
114        Sharing::Beside => vec![take(COMMANDS, libc::LOCK_SH)?],
115    };
116    Ok(DataDirLock { _held: held })
117}
118
119const SERVERS: &str = "lock";
120const COMMANDS: &str = "lock.commands";
121
122/// Opens `path`, made if missing, and locks it without waiting.
123fn flock(path: &Path, how: libc::c_int) -> io::Result<File> {
124    let file = OpenOptions::new()
125        .create(true)
126        .truncate(false)
127        .write(true)
128        .open(path)?;
129    // SAFETY: the descriptor is this open file's, and flock takes only it
130    // and flags.
131    if unsafe { libc::flock(file.as_raw_fd(), how | libc::LOCK_NB) } != 0 {
132        return Err(io::Error::last_os_error());
133    }
134    Ok(file)
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn rekey_check_and_tenant_create_and_remove_run_alone() {
143        for (work, sharing) in [
144            (Work::Serve, Sharing::Serve),
145            (Work::TenantCreate, Sharing::Alone),
146            (Work::TenantList, Sharing::Beside),
147            (Work::TenantSetRetention, Sharing::Beside),
148            (Work::TenantRemove, Sharing::Alone),
149            (Work::KeyCreate, Sharing::Beside),
150            (Work::KeyList, Sharing::Beside),
151            (Work::KeyRevoke, Sharing::Beside),
152            (Work::RekeyTenant, Sharing::Alone),
153            (Work::RekeyMaster, Sharing::Alone),
154            (Work::AuditExport, Sharing::Beside),
155            (Work::Check, Sharing::Alone),
156        ] {
157            assert_eq!(work.sharing(), sharing, "{work:?}");
158        }
159    }
160
161    /// Each pair: what holds the directory, what asks for it next, and
162    /// whether that is allowed.
163    #[test]
164    fn who_may_run_beside_whom() {
165        use Sharing::{Alone, Beside, Serve};
166        for (first, second, allowed) in [
167            (Serve, Serve, false),
168            (Serve, Alone, false),
169            (Serve, Beside, true),
170            (Alone, Serve, false),
171            (Alone, Alone, false),
172            (Alone, Beside, false),
173            (Beside, Serve, true),
174            (Beside, Alone, false),
175            (Beside, Beside, true),
176        ] {
177            let dir = tempfile::tempdir().expect("temp dir");
178            let held = lock(dir.path(), first).expect("the first lock");
179            match lock(dir.path(), second) {
180                Ok(_) => assert!(allowed, "{second:?} beside {first:?}"),
181                Err(LockError::InUse(asked)) => {
182                    assert!(!allowed, "{second:?} refused beside {first:?}");
183                    assert_eq!(asked, second);
184                }
185                Err(e) => panic!("{first:?} then {second:?}: {e}"),
186            }
187            drop(held);
188            assert!(lock(dir.path(), second).is_ok(), "{second:?} once free");
189        }
190    }
191
192    /// A refused command that runs alone leaves nothing held: a server can
193    /// still start once the beside command ends.
194    #[test]
195    fn a_refused_lock_holds_nothing() {
196        let dir = tempfile::tempdir().expect("temp dir");
197        let beside = lock(dir.path(), Sharing::Beside).expect("beside");
198        assert!(lock(dir.path(), Sharing::Alone).is_err());
199        let server = lock(dir.path(), Sharing::Serve).expect("serve beside");
200        drop((beside, server));
201        assert!(lock(dir.path(), Sharing::Alone).is_ok());
202    }
203
204    #[test]
205    fn each_refusal_says_what_holds_the_directory() {
206        for (sharing, words) in [
207            (Sharing::Serve, "another vcon-server or a command"),
208            (Sharing::Alone, "stop the server"),
209            (Sharing::Beside, "runs alone"),
210        ] {
211            let text = LockError::InUse(sharing).to_string();
212            assert!(text.contains(words), "{sharing:?}: {text}");
213        }
214    }
215
216    #[test]
217    fn a_data_directory_that_cannot_hold_a_lock_file_is_an_error() {
218        let missing = Path::new("/nonexistent/vcon-server-data");
219        for sharing in [Sharing::Serve, Sharing::Alone, Sharing::Beside] {
220            assert!(matches!(lock(missing, sharing), Err(LockError::Io(_))));
221        }
222    }
223}