1use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, Weak};
20
21use tokio::sync::{Mutex, OwnedMutexGuard};
22
23#[derive(Default)]
39pub struct FileWriteLocks {
40 locks: parking_lot::Mutex<HashMap<PathBuf, Weak<Mutex<()>>>>,
41}
42
43impl FileWriteLocks {
44 fn mutex_for(&self, path: &Path) -> Arc<Mutex<()>> {
46 let mut locks = self.locks.lock();
47 locks.retain(|_, weak| weak.strong_count() > 0);
48 if let Some(mutex) = locks.get(path).and_then(Weak::upgrade) {
49 return mutex;
50 }
51 let mutex = Arc::new(Mutex::new(()));
52 locks.insert(path.to_path_buf(), Arc::downgrade(&mutex));
53 mutex
54 }
55
56 pub async fn lock(&self, path: &Path) -> OwnedMutexGuard<()> {
58 self.mutex_for(path).lock_owned().await
59 }
60
61 pub fn lock_blocking(&self, path: &Path) -> OwnedMutexGuard<()> {
68 self.mutex_for(path).blocking_lock_owned()
69 }
70
71 pub fn lock_all_blocking(&self, paths: &[&Path]) -> Vec<OwnedMutexGuard<()>> {
74 let mut ordered: Vec<&Path> = paths.to_vec();
75 ordered.sort_unstable();
76 ordered.dedup();
77 ordered
78 .into_iter()
79 .map(|path| self.lock_blocking(path))
80 .collect()
81 }
82
83 #[cfg(test)]
85 fn live(&self) -> usize {
86 self.locks
87 .lock()
88 .values()
89 .filter(|weak| weak.strong_count() > 0)
90 .count()
91 }
92}
93
94pub use mbr_core::vault::{atomic_write, create_unique_temp_file};
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn temp_files_in(dir: &Path) -> Vec<String> {
105 std::fs::read_dir(dir)
106 .expect("read dir")
107 .filter_map(|entry| entry.ok()?.file_name().into_string().ok())
108 .filter(|name| name.ends_with(".mbr-tmp"))
109 .collect()
110 }
111
112 #[test]
113 fn unique_temp_files_never_share_a_name() {
114 let temp = tempfile::tempdir().expect("temp dir");
115 let (a, _fa) = create_unique_temp_file(temp.path(), "note.md").expect("first");
116 let (b, _fb) = create_unique_temp_file(temp.path(), "note.md").expect("second");
117 assert_ne!(a, b);
118 for path in [&a, &b] {
119 let name = path.file_name().and_then(|n| n.to_str()).expect("name");
120 assert!(
121 name.starts_with(".note.md.") && name.ends_with(".mbr-tmp"),
122 "{name}"
123 );
124 }
125 }
126
127 #[test]
131 fn concurrent_atomic_writes_all_succeed_and_leave_no_temp_files() {
132 let temp = tempfile::tempdir().expect("temp dir");
133 let path = temp.path().join("note.md");
134 std::fs::write(&path, "start\n").expect("seed");
135 let results: Vec<_> = std::thread::scope(|scope| {
136 (0..16)
137 .map(|i| {
138 let path = &path;
139 scope.spawn(move || atomic_write(path, format!("writer {i}\n").as_bytes()))
140 })
141 .collect::<Vec<_>>()
142 .into_iter()
143 .map(|handle| handle.join().expect("writer thread"))
144 .collect()
145 });
146 for result in &results {
147 assert!(result.is_ok(), "{result:?}");
148 }
149 let text = std::fs::read_to_string(&path).expect("read back");
150 assert!(text.starts_with("writer "), "{text}");
151 assert_eq!(temp_files_in(temp.path()), Vec::<String>::new());
152 }
153
154 #[cfg(unix)]
155 #[test]
156 fn atomic_write_keeps_the_file_permissions() {
157 use std::os::unix::fs::PermissionsExt;
158 let temp = tempfile::tempdir().expect("temp dir");
159 let path = temp.path().join("private.md");
160 std::fs::write(&path, "secret\n").expect("seed");
161 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).expect("chmod");
162 atomic_write(&path, b"still secret\n").expect("write");
163 let mode = std::fs::metadata(&path).expect("stat").permissions().mode();
164 assert_eq!(mode & 0o777, 0o600);
165 assert_eq!(std::fs::read_to_string(&path).unwrap(), "still secret\n");
166 }
167
168 #[tokio::test]
169 async fn locks_serialize_one_path_and_forget_it_afterwards() {
170 let locks = Arc::new(FileWriteLocks::default());
171 let a = Path::new("/repo/a.md");
172 let guard = locks.lock(a).await;
173 drop(locks.lock(Path::new("/repo/b.md")).await);
175
176 let waiter = {
178 let locks = Arc::clone(&locks);
179 tokio::spawn(async move { drop(locks.lock(Path::new("/repo/a.md")).await) })
180 };
181 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
182 assert!(!waiter.is_finished(), "second lock on a.md must wait");
183 drop(guard);
184 waiter.await.expect("waiter");
185
186 assert_eq!(locks.live(), 0);
188 drop(locks.lock(a).await);
189 assert!(locks.locks.lock().len() <= 1, "dead entries are swept");
190 }
191
192 #[tokio::test]
194 async fn blocking_and_async_locks_exclude_each_other() {
195 let locks = Arc::new(FileWriteLocks::default());
196 let guard = locks.lock(Path::new("/repo/a.md")).await;
197 let blocked = {
198 let locks = Arc::clone(&locks);
199 tokio::task::spawn_blocking(move || drop(locks.lock_blocking(Path::new("/repo/a.md"))))
200 };
201 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
202 assert!(
203 !blocked.is_finished(),
204 "blocking lock must wait for async holder"
205 );
206 drop(guard);
207 blocked.await.expect("blocking locker");
208 }
209
210 #[test]
213 fn lock_all_blocking_orders_paths_and_dedups() {
214 let locks = Arc::new(FileWriteLocks::default());
215 let (a, b) = (Path::new("/repo/a.md"), Path::new("/repo/b.md"));
216 std::thread::scope(|scope| {
217 for round in 0..200 {
218 let locks = &locks;
219 let (first, second) = if round % 2 == 0 { (a, b) } else { (b, a) };
220 scope.spawn(move || drop(locks.lock_all_blocking(&[first, second])));
221 }
222 });
223 assert_eq!(locks.lock_all_blocking(&[a, a]).len(), 1);
225 }
226}