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