hennery_kernel/
coordination.rs1use anyhow::{Context, Result, bail};
5use std::collections::HashMap;
6use std::os::fd::AsRawFd;
7use std::os::unix::fs::MetadataExt;
8use std::os::unix::fs::OpenOptionsExt;
9use std::path::Path;
10use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
11
12#[derive(Debug, Default)]
15pub struct Gate {
16 state: Mutex<GateState>,
17 ready: Condvar,
18}
19#[derive(Debug, Default)]
20struct GateState {
21 owner: Option<std::thread::ThreadId>,
22 depth: usize,
23}
24pub struct GateGuard {
25 gate: Arc<Gate>,
26 _thread: std::marker::PhantomData<std::rc::Rc<()>>,
27}
28impl Gate {
29 pub fn lock(self: &Arc<Self>) -> GateGuard {
30 let thread = std::thread::current().id();
31 let mut state = self.state.lock().expect("lifecycle gate");
32 while state.owner.is_some_and(|owner| owner != thread) {
33 state = self.ready.wait(state).expect("lifecycle gate");
34 }
35 state.owner = Some(thread);
36 state.depth += 1;
37 GateGuard {
38 gate: self.clone(),
39 _thread: std::marker::PhantomData,
40 }
41 }
42}
43impl Drop for GateGuard {
44 fn drop(&mut self) {
45 let mut state = self.gate.state.lock().expect("lifecycle gate");
46 state.depth -= 1;
47 if state.depth == 0 {
48 state.owner = None;
49 self.gate.ready.notify_all();
50 }
51 }
52}
53
54#[derive(Debug, Default)]
55pub struct Coordination {
56 pub gate: Arc<Gate>,
57 _database: Option<std::fs::File>,
60 _ownership: Option<std::fs::File>,
61}
62
63impl Coordination {
64 pub(crate) fn acquire(file: std::fs::File, path: &Path) -> Result<Arc<Self>> {
65 type Registry = Mutex<HashMap<(u64, u64), Weak<Coordination>>>;
66 static OWNERS: OnceLock<Registry> = OnceLock::new();
67 let metadata = file.metadata()?;
68 anyhow::ensure!(metadata.nlink() == 1, "hard-linked databases are refused");
69 let identity = (metadata.dev(), metadata.ino());
70 let mut owners = OWNERS.get_or_init(Mutex::default).lock().expect("writer registry");
71 if let Some(owner) = owners.get(&identity).and_then(Weak::upgrade) {
72 return Ok(owner);
73 }
74 let canonical = path.canonicalize()?;
77 let mut lock_path = canonical.as_os_str().to_os_string();
78 lock_path.push(".writer.lock");
79 let lock_file = std::fs::OpenOptions::new()
80 .read(true)
81 .write(true)
82 .create(true)
83 .truncate(false)
84 .mode(0o600)
85 .custom_flags(libc::O_NOFOLLOW)
86 .open(lock_path)?;
87 anyhow::ensure!(
88 lock_file.metadata()?.nlink() == 1,
89 "hard-linked writer locks are refused"
90 );
91 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
93 while unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
94 let err = std::io::Error::last_os_error();
95 if err.raw_os_error() == Some(libc::EWOULDBLOCK) {
96 if std::time::Instant::now() < deadline {
97 std::thread::sleep(std::time::Duration::from_millis(20));
98 continue;
99 }
100 bail!("another process owns this database writer");
101 }
102 return Err(err).context("acquire database writer ownership");
103 }
104 let owner = Arc::new(Self {
105 gate: Arc::default(),
106 _database: Some(file),
107 _ownership: Some(lock_file),
108 });
109 owners.insert(identity, Arc::downgrade(&owner));
110 Ok(owner)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn inspect_descriptor(descriptor: std::os::fd::RawFd) -> std::io::Result<libc::stat> {
119 let mut metadata = std::mem::MaybeUninit::<libc::stat>::uninit();
122 if unsafe { libc::fstat(descriptor, metadata.as_mut_ptr()) } != 0 {
123 return Err(std::io::Error::last_os_error());
124 }
125 Ok(unsafe { metadata.assume_init() })
126 }
127
128 #[test]
129 fn registry_identity_stays_pinned_until_coordination_is_released() {
130 let dir = tempfile::tempdir().unwrap();
131 let path = dir.path().join("hennery.db");
132 let file = std::fs::File::create(&path).unwrap();
133 let descriptor = file.as_raw_fd();
134 let identity = inspect_descriptor(descriptor).unwrap();
135 let owner = Coordination::acquire(file, &path).unwrap();
136 std::fs::remove_file(&path).unwrap();
139 let metadata = inspect_descriptor(descriptor).expect("registry identity descriptor was closed");
140 assert_eq!((metadata.st_dev, metadata.st_ino), (identity.st_dev, identity.st_ino));
141 assert_eq!(metadata.st_nlink, 0);
142 drop(owner);
143 }
144}