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    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        .open(path)?;
131    // SAFETY: the descriptor is this open file's, and flock takes only it
132    // and flags.
133    if unsafe { libc::flock(file.as_raw_fd(), how | libc::LOCK_NB) } != 0 {
134        return Err(io::Error::last_os_error());
135    }
136    Ok(file)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn rekey_check_and_tenant_create_and_remove_run_alone() {
145        for (work, sharing) in [
146            (Work::Serve, Sharing::Serve),
147            (Work::TenantCreate, Sharing::Alone),
148            (Work::TenantList, Sharing::Beside),
149            (Work::TenantSetRetention, Sharing::Beside),
150            (Work::TenantRemove, Sharing::Alone),
151            (Work::KeyCreate, Sharing::Beside),
152            (Work::KeyList, Sharing::Beside),
153            (Work::KeyRevoke, Sharing::Beside),
154            (Work::RekeyTenant, Sharing::Alone),
155            (Work::RekeyMaster, Sharing::Alone),
156            (Work::AuditExport, Sharing::Beside),
157            (Work::Check, Sharing::Alone),
158        ] {
159            assert_eq!(work.sharing(), sharing, "{work:?}");
160        }
161    }
162
163    /// Each pair: what holds the directory, what asks for it next, and
164    /// whether that is allowed.
165    #[test]
166    fn who_may_run_beside_whom() {
167        use Sharing::{Alone, Beside, Serve};
168        for (first, second, allowed) in [
169            (Serve, Serve, false),
170            (Serve, Alone, false),
171            (Serve, Beside, true),
172            (Alone, Serve, false),
173            (Alone, Alone, false),
174            (Alone, Beside, false),
175            (Beside, Serve, true),
176            (Beside, Alone, false),
177            (Beside, Beside, true),
178        ] {
179            let dir = tempfile::tempdir().expect("temp dir");
180            let held = lock(dir.path(), first).expect("the first lock");
181            match lock(dir.path(), second) {
182                Ok(_) => assert!(allowed, "{second:?} beside {first:?}"),
183                Err(LockError::InUse(asked)) => {
184                    assert!(!allowed, "{second:?} refused beside {first:?}");
185                    assert_eq!(asked, second);
186                }
187                Err(e) => panic!("{first:?} then {second:?}: {e}"),
188            }
189            drop(held);
190            assert!(lock(dir.path(), second).is_ok(), "{second:?} once free");
191        }
192    }
193
194    /// A refused command that runs alone leaves nothing held: a server can
195    /// still start once the beside command ends.
196    #[test]
197    fn a_refused_lock_holds_nothing() {
198        let dir = tempfile::tempdir().expect("temp dir");
199        let beside = lock(dir.path(), Sharing::Beside).expect("beside");
200        assert!(lock(dir.path(), Sharing::Alone).is_err());
201        let server = lock(dir.path(), Sharing::Serve).expect("serve beside");
202        drop((beside, server));
203        assert!(lock(dir.path(), Sharing::Alone).is_ok());
204    }
205
206    #[test]
207    fn each_refusal_says_what_holds_the_directory() {
208        for (sharing, words) in [
209            (Sharing::Serve, "another vcon-server or a command"),
210            (Sharing::Alone, "stop the server"),
211            (Sharing::Beside, "runs alone"),
212        ] {
213            let text = LockError::InUse(sharing).to_string();
214            assert!(text.contains(words), "{sharing:?}: {text}");
215        }
216    }
217
218    #[test]
219    fn a_data_directory_that_cannot_hold_a_lock_file_is_an_error() {
220        let missing = Path::new("/nonexistent/vcon-server-data");
221        for sharing in [Sharing::Serve, Sharing::Alone, Sharing::Beside] {
222            assert!(matches!(lock(missing, sharing), Err(LockError::Io(_))));
223        }
224    }
225}