|
|
… |
64 unchanged lines |
| 65 |
65 |
|
/// The log's name in its checkpoints: the hub's host and `/log`. |
| 66 |
66 |
|
pub origin: String, |
| 67 |
67 |
|
files: Mutex<Files>, |
|
68 |
+ |
/// `append.lock`: held, exclusively, around each append, so several |
|
69 |
+ |
/// processes sharing the data directory append one at a time (#274). |
|
70 |
+ |
lock: File, |
| 68 |
71 |
|
/// The latest signed checkpoint: its size and its note. |
| 69 |
72 |
|
checkpoint: Mutex<Option<(u64, String)>>, |
| 70 |
73 |
|
/// Witnesses this log asks to cosign its checkpoints. |
|
|
… |
1 unchanged lines |
| 72 |
75 |
|
/// Logs this hub witnesses: their origins and keys. |
| 73 |
76 |
|
witness_for: std::sync::RwLock<Vec<(String, Identity)>>, |
| 74 |
77 |
|
/// Whether this process writes the log: the web role's (#272). A |
| 75 |
|
- |
/// process without it only reads what the log held when it opened. |
|
78 |
+ |
/// process without it only reads, catching up with the writers. |
| 76 |
79 |
|
writer: bool, |
| 77 |
80 |
|
} |
| 78 |
81 |
|
|
|
|
… |
27 unchanged lines |
| 106 |
109 |
|
|
| 107 |
110 |
|
/// Open the log to read only, as a process without the web role does |
| 108 |
111 |
|
/// (#272): it neither appends nor signs, and leaves the files as they |
| 109 |
|
- |
/// are, so it never cuts short an append the writer is making. Until |
| 110 |
|
- |
/// the log has a cluster-wide writer (#274), one process per data |
| 111 |
|
- |
/// directory has the web role. |
|
112 |
+ |
/// are, so it never cuts short an append a writer is making. Several |
|
113 |
+ |
/// web processes may share the data directory and write (#274). |
| 112 |
114 |
|
pub fn open_reader(dir: &Path, origin: &str) -> anyhow::Result<Log> { |
| 113 |
115 |
|
Self::open_as(dir, origin, false) |
| 114 |
116 |
|
} |
|
|
… |
2 unchanged lines |
| 117 |
119 |
|
std::fs::create_dir_all(dir)?; |
| 118 |
120 |
|
let open = |name: &str| OpenOptions::new().read(true).append(true).create(true).open(dir.join(name)); |
| 119 |
121 |
|
let (entries, offsets, hashes) = (open("entries")?, open("offsets")?, open("hashes")?); |
| 120 |
|
- |
let entries_on_disk = entries.metadata()?.len(); |
| 121 |
|
- |
// The whole entries: those whose offset was written and whose |
| 122 |
|
- |
// bytes all were. |
| 123 |
|
- |
let mut size = offsets.metadata()?.len() / 8; |
|
122 |
+ |
let lock = OpenOptions::new().read(true).write(true).create(true).truncate(false).open(dir.join("append.lock"))?; |
|
123 |
+ |
let mut files = Files { entries, offsets, hashes, size: 0, entries_len: 0 }; |
|
124 |
+ |
if writer { |
|
125 |
+ |
lock.lock()?; |
|
126 |
+ |
let done = Self::recover(&mut files, true); |
|
127 |
+ |
lock.unlock()?; |
|
128 |
+ |
done?; |
|
129 |
+ |
} else { |
|
130 |
+ |
Self::recover(&mut files, false)?; |
|
131 |
+ |
} |
|
132 |
+ |
let size = files.size; |
|
133 |
+ |
Ok(Log { |
|
134 |
+ |
dir: dir.to_path_buf(), |
|
135 |
+ |
origin: origin.to_string(), |
|
136 |
+ |
files: Mutex::new(files), |
|
137 |
+ |
lock, |
|
138 |
+ |
checkpoint: Mutex::new(Self::read_checkpoint(dir, size)), |
|
139 |
+ |
witnesses: Default::default(), |
|
140 |
+ |
witness_for: Default::default(), |
|
141 |
+ |
writer, |
|
142 |
+ |
}) |
|
143 |
+ |
} |
|
144 |
+ |
|
|
145 |
+ |
/// Read the log's extent from its files, as another process may have |
|
146 |
+ |
/// appended since (#274). The whole entries count: those whose offset |
|
147 |
+ |
/// and bytes were written, and whose stored hashes all were. A writer, |
|
148 |
+ |
/// holding the append lock, also finishes what an append cut short left |
|
149 |
+ |
/// (it truncates partial bytes and computes missing hashes again); a |
|
150 |
+ |
/// reader changes nothing and takes the whole entries only. |
|
151 |
+ |
fn recover(files: &mut Files, writer: bool) -> anyhow::Result<()> { |
|
152 |
+ |
let entries_on_disk = files.entries.metadata()?.len(); |
|
153 |
+ |
let mut size = files.offsets.metadata()?.len() / 8; |
| 124 |
154 |
|
let mut entries_len = 0; |
| 125 |
155 |
|
while size > 0 { |
| 126 |
|
- |
let off = read_u64_at(&offsets, (size - 1) * 8)?; |
|
156 |
+ |
let off = read_u64_at(&files.offsets, (size - 1) * 8)?; |
| 127 |
157 |
|
let mut len = [0u8; 2]; |
| 128 |
|
- |
if off + 2 <= entries_on_disk && entries.read_exact_at(&mut len, off).is_ok() { |
|
158 |
+ |
if off + 2 <= entries_on_disk && files.entries.read_exact_at(&mut len, off).is_ok() { |
| 129 |
159 |
|
let end = off + 2 + u16::from_be_bytes(len) as u64; |
| 130 |
160 |
|
if end <= entries_on_disk { |
| 131 |
161 |
|
entries_len = end; |
|
|
… |
4 unchanged lines |
| 136 |
166 |
|
} |
| 137 |
167 |
|
// The hashes of every leaf whose hashes were all written; the rest |
| 138 |
168 |
|
// are computed again from the entries. |
| 139 |
|
- |
let have = hashes.metadata()?.len() / 32; |
|
169 |
+ |
let have = files.hashes.metadata()?.len() / 32; |
| 140 |
170 |
|
let mut whole = size; |
| 141 |
171 |
|
while tlog::stored_hash_count(whole) > have { |
| 142 |
172 |
|
whole -= 1; |
| 143 |
173 |
|
} |
| 144 |
174 |
|
if !writer { |
| 145 |
|
- |
// A reader takes the leaves whose hashes are all there and |
| 146 |
|
- |
// changes nothing. |
| 147 |
|
- |
let files = Files { entries, offsets, hashes, size: whole, entries_len }; |
| 148 |
|
- |
return Ok(Log { |
| 149 |
|
- |
dir: dir.to_path_buf(), |
| 150 |
|
- |
origin: origin.to_string(), |
| 151 |
|
- |
files: Mutex::new(files), |
| 152 |
|
- |
checkpoint: Mutex::new(Self::read_checkpoint(dir, whole)), |
| 153 |
|
- |
witnesses: Default::default(), |
| 154 |
|
- |
witness_for: Default::default(), |
| 155 |
|
- |
writer, |
| 156 |
|
- |
}); |
|
175 |
+ |
files.size = whole; |
|
176 |
+ |
files.entries_len = entries_len; |
|
177 |
+ |
return Ok(()); |
| 157 |
178 |
|
} |
| 158 |
|
- |
offsets.set_len(size * 8)?; |
| 159 |
|
- |
entries.set_len(entries_len)?; |
| 160 |
|
- |
hashes.set_len(tlog::stored_hash_count(whole) * 32)?; |
| 161 |
|
- |
let mut files = Files { entries, offsets, hashes, size: whole, entries_len }; |
|
179 |
+ |
files.offsets.set_len(size * 8)?; |
|
180 |
+ |
files.entries.set_len(entries_len)?; |
|
181 |
+ |
files.hashes.set_len(tlog::stored_hash_count(whole) * 32)?; |
|
182 |
+ |
files.entries_len = entries_len; |
| 162 |
183 |
|
for n in whole..size { |
| 163 |
|
- |
let bytes = Self::entry_bytes(&files, n)?; |
|
184 |
+ |
let bytes = Self::entry_bytes(files, n)?; |
| 164 |
185 |
|
let add = tlog::stored_hashes_for_leaf(n, tlog::leaf_hash(&bytes), &FileHashes(&files.hashes))?; |
| 165 |
186 |
|
for h in add { |
| 166 |
187 |
|
files.hashes.write_all(&h)?; |
|
|
… |
2 unchanged lines |
| 169 |
190 |
|
} |
| 170 |
191 |
|
files.size = size; |
| 171 |
192 |
|
files.hashes.sync_data()?; |
| 172 |
|
- |
Ok(Log { |
| 173 |
|
- |
dir: dir.to_path_buf(), |
| 174 |
|
- |
origin: origin.to_string(), |
| 175 |
|
- |
files: Mutex::new(files), |
| 176 |
|
- |
checkpoint: Mutex::new(Self::read_checkpoint(dir, size)), |
| 177 |
|
- |
witnesses: Default::default(), |
| 178 |
|
- |
witness_for: Default::default(), |
| 179 |
|
- |
writer, |
| 180 |
|
- |
}) |
|
193 |
+ |
Ok(()) |
|
194 |
+ |
} |
|
195 |
+ |
|
|
196 |
+ |
/// Catch up with what other processes appended (#274). |
|
197 |
+ |
fn catch_up(&self, f: &mut Files) { |
|
198 |
+ |
if let Err(e) = Self::recover(f, false) { |
|
199 |
+ |
tracing::warn!("tlog: reading the log's extent: {e:#}"); |
|
200 |
+ |
} |
| 181 |
201 |
|
} |
| 182 |
202 |
|
|
| 183 |
203 |
|
/// The signed checkpoint kept in `dir`, if it is of at most `size` entries. |
|
|
… |
16 unchanged lines |
| 200 |
220 |
|
|
| 201 |
221 |
|
/// Entry `n`'s bytes. |
| 202 |
222 |
|
pub fn entry(&self, n: u64) -> anyhow::Result<Vec<u8>> { |
| 203 |
|
- |
let f = self.files.lock().unwrap(); |
|
223 |
+ |
let mut f = self.files.lock().unwrap(); |
|
224 |
+ |
if n >= f.size { |
|
225 |
+ |
self.catch_up(&mut f); |
|
226 |
+ |
} |
| 204 |
227 |
|
anyhow::ensure!(n < f.size, "entry {n} is not in the log"); |
| 205 |
228 |
|
Self::entry_bytes(&f, n) |
| 206 |
229 |
|
} |
| 207 |
230 |
|
|
| 208 |
231 |
|
/// Entries in the log. |
| 209 |
232 |
|
pub fn size(&self) -> u64 { |
| 210 |
|
- |
self.files.lock().unwrap().size |
|
233 |
+ |
let mut f = self.files.lock().unwrap(); |
|
234 |
+ |
self.catch_up(&mut f); |
|
235 |
+ |
f.size |
| 211 |
236 |
|
} |
| 212 |
237 |
|
|
| 213 |
238 |
|
/// Append entries, durably; their numbers. |
|
|
… |
2 unchanged lines |
| 216 |
241 |
|
anyhow::bail!("this process does not write the log: it has no web role"); |
| 217 |
242 |
|
} |
| 218 |
243 |
|
let mut f = self.files.lock().unwrap(); |
|
244 |
+ |
// One append at a time across the processes sharing the directory |
|
245 |
+ |
// (#274), each from the log as the last left it. |
|
246 |
+ |
self.lock.lock()?; |
|
247 |
+ |
let appended = Self::recover(&mut f, true).and_then(|()| Self::append_locked(&mut f, entries)); |
|
248 |
+ |
self.lock.unlock()?; |
|
249 |
+ |
appended |
|
250 |
+ |
} |
|
251 |
+ |
|
|
252 |
+ |
fn append_locked(f: &mut Files, entries: &[Entry]) -> anyhow::Result<Vec<u64>> { |
| 219 |
253 |
|
let mut out = Vec::with_capacity(entries.len()); |
| 220 |
254 |
|
for e in entries { |
| 221 |
255 |
|
let bytes = e.encode(); |
|
|
… |
118 unchanged lines |
| 340 |
374 |
|
Ok(()) |
| 341 |
375 |
|
} |
| 342 |
376 |
|
|
| 343 |
|
- |
/// The latest signed checkpoint note. |
|
377 |
+ |
/// The latest signed checkpoint note: the newer of this process's and |
|
378 |
+ |
/// the one kept in the directory, which the process leading the |
|
379 |
+ |
/// checkpointer signs (#274). |
| 344 |
380 |
|
pub fn checkpoint(&self) -> Option<String> { |
| 345 |
|
- |
self.checkpoint.lock().unwrap().as_ref().map(|(_, n)| n.clone()) |
|
381 |
+ |
let mut held = self.checkpoint.lock().unwrap(); |
|
382 |
+ |
if let Some(kept) = Self::read_checkpoint(&self.dir, u64::MAX) { |
|
383 |
+ |
if held.as_ref().is_none_or(|(s, _)| kept.0 > *s) { |
|
384 |
+ |
*held = Some(kept); |
|
385 |
+ |
} |
|
386 |
+ |
} |
|
387 |
+ |
held.as_ref().map(|(_, n)| n.clone()) |
| 346 |
388 |
|
} |
| 347 |
389 |
|
|
| 348 |
390 |
|
/// Sign a checkpoint of the log as it is, unless the last one already |
|
|
… |
3 unchanged lines |
| 352 |
394 |
|
return Ok(false); |
| 353 |
395 |
|
} |
| 354 |
396 |
|
let size = self.size(); |
|
397 |
+ |
// From the newest kept, whichever process signed it. |
|
398 |
+ |
let _ = self.checkpoint(); |
| 355 |
399 |
|
if self.checkpoint.lock().unwrap().as_ref().is_some_and(|(s, _)| *s >= size) { |
| 356 |
400 |
|
return Ok(false); |
| 357 |
401 |
|
} |
|
|
… |
453 unchanged lines |
| 811 |
855 |
|
assert!(log.tile(&Tile { level: 0, n: 1, width: 46 }).unwrap().is_none(), "not yet"); |
| 812 |
856 |
|
let bundle = log.bundle(&Tile { level: 0, n: 1, width: 45 }).unwrap().unwrap(); |
| 813 |
857 |
|
assert_eq!(tlog::decode_bundle(&bundle).unwrap()[0], first[256].encode()); |
|
858 |
+ |
} |
|
859 |
+ |
|
|
860 |
+ |
/// oxygenhub#274: writers sharing the directory (each its own handles, |
|
861 |
+ |
/// as separate processes have) append one at a time, each from the log |
|
862 |
+ |
/// as the others left it; readers and checkpoints follow. |
|
863 |
+ |
#[test] |
|
864 |
+ |
fn writers_sharing_the_directory_append_one_at_a_time() { |
|
865 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
866 |
+ |
let a = std::sync::Arc::new(Log::open(dir.path(), "hub.example/log").unwrap()); |
|
867 |
+ |
let b = std::sync::Arc::new(Log::open(dir.path(), "hub.example/log").unwrap()); |
|
868 |
+ |
let reader = Log::open_reader(dir.path(), "hub.example/log").unwrap(); |
|
869 |
+ |
let spawn = |log: std::sync::Arc<Log>, from: u64| { |
|
870 |
+ |
std::thread::spawn(move || { |
|
871 |
+ |
let mut got = Vec::new(); |
|
872 |
+ |
for i in 0..150 { |
|
873 |
+ |
got.extend(log.append(&[entry(from + i)]).unwrap()); |
|
874 |
+ |
} |
|
875 |
+ |
got |
|
876 |
+ |
}) |
|
877 |
+ |
}; |
|
878 |
+ |
let (ta, tb) = (spawn(a.clone(), 0), spawn(b.clone(), 1000)); |
|
879 |
+ |
let mut numbers: Vec<u64> = ta.join().unwrap().into_iter().chain(tb.join().unwrap()).collect(); |
|
880 |
+ |
numbers.sort(); |
|
881 |
+ |
assert_eq!(numbers, (0..300).collect::<Vec<_>>(), "every entry its own place"); |
|
882 |
+ |
assert_eq!((a.size(), b.size(), reader.size()), (300, 300, 300)); |
|
883 |
+ |
let root = a.tree_hash(300).unwrap(); |
|
884 |
+ |
assert_eq!(b.tree_hash(300).unwrap(), root); |
|
885 |
+ |
let fresh = Log::open(dir.path(), "hub.example/log").unwrap(); |
|
886 |
+ |
assert_eq!(fresh.tree_hash(300).unwrap(), root, "the files are one log"); |
|
887 |
+ |
assert_eq!(b.entry(299).unwrap(), fresh.entry(299).unwrap()); |
|
888 |
+ |
assert_eq!(reader.entry(299).unwrap(), fresh.entry(299).unwrap()); |
|
889 |
+ |
// A checkpoint one signs, the other serves. |
|
890 |
+ |
let sk = oxy_core::identity::SecretKey::generate(); |
|
891 |
+ |
let hub = sk.identity("hub", "[email protected]", 1); |
|
892 |
+ |
assert!(a.refresh_checkpoint(&hub, &sk).unwrap()); |
|
893 |
+ |
assert_eq!(b.checkpoint(), a.checkpoint()); |
|
894 |
+ |
assert!(!b.refresh_checkpoint(&hub, &sk).unwrap(), "already covered by the other's"); |
| 814 |
895 |
|
} |
| 815 |
896 |
|
|
| 816 |
897 |
|
#[test] |
|
|
… |
19 unchanged lines |