1#[cfg(any(test, feature = "test-support"))]
4pub mod crash;
5#[cfg(any(test, feature = "test-support"))]
6pub mod fault;
7
8use std::fs::{File, OpenOptions};
9use std::io::{IoSlice, Read, Seek, SeekFrom, Write};
10#[cfg(unix)]
11use std::os::fd::AsRawFd;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum SyncKind {
19 Barrier,
24 Full,
26}
27
28pub trait VfsFile: Send {
30 fn append(&mut self, bytes: &[u8]) -> std::io::Result<()>;
32 fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
38 for buffer in buffers {
39 self.append(buffer)?;
40 }
41 Ok(())
42 }
43 fn sync(&self, kind: SyncKind) -> std::io::Result<()>;
45}
46
47pub trait Vfs: Send + Sync {
49 #[doc(hidden)]
51 fn segment_data_read_counter(&self) -> Option<Arc<AtomicU64>> {
52 None
53 }
54 fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool>;
57 fn open(&self, path: &Path) -> std::io::Result<u64>;
59 fn open_for_map(&self, path: &Path) -> std::io::Result<File>;
66 fn read(&self, path: &Path) -> std::io::Result<Vec<u8>>;
68 fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>>;
70 fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()>;
72 fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn VfsFile>>;
74 fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()>;
76 fn sync(&self, path: &Path, kind: SyncKind) -> std::io::Result<()>;
78 fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>>;
80 fn delete(&self, path: &Path) -> std::io::Result<()>;
82}
83
84#[derive(Clone, Copy, Debug, Default)]
86pub struct StdVfs;
87
88struct StdVfsFile(File);
89
90impl VfsFile for StdVfsFile {
91 fn append(&mut self, bytes: &[u8]) -> std::io::Result<()> {
92 self.0.write_all(bytes)
93 }
94
95 fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
96 let mut remaining = buffers;
97 while !remaining.is_empty() {
98 let written = self.0.write_vectored(remaining)?;
99 if written == 0 {
100 return Err(std::io::Error::new(
101 std::io::ErrorKind::WriteZero,
102 "failed to append vectored WAL group",
103 ));
104 }
105 IoSlice::advance_slices(&mut remaining, written);
106 }
107 Ok(())
108 }
109
110 fn sync(&self, kind: SyncKind) -> std::io::Result<()> {
111 sync_file(&self.0, kind)
112 }
113}
114
115impl Vfs for StdVfs {
116 fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool> {
117 if create {
118 std::fs::create_dir_all(path)?;
119 }
120 Ok(std::fs::metadata(path)?.is_dir())
121 }
122
123 fn open(&self, path: &Path) -> std::io::Result<u64> {
124 Ok(File::open(path)?.metadata()?.len())
125 }
126
127 fn open_for_map(&self, path: &Path) -> std::io::Result<File> {
128 File::open(path)
129 }
130
131 fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
132 let mut file = File::open(path)?;
133 let length = usize::try_from(file.metadata()?.len()).map_err(|_| {
134 std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds usize")
135 })?;
136 let mut bytes = Vec::with_capacity(length);
137 file.read_to_end(&mut bytes)?;
138 Ok(bytes)
139 }
140
141 fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>> {
142 let mut file = File::open(path)?;
143 file.seek(SeekFrom::Start(offset))?;
144 let mut bytes = vec![0_u8; length];
145 let mut filled = 0_usize;
146 while filled < length {
147 let remaining = bytes.get_mut(filled..).ok_or_else(|| {
148 std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid read range")
149 })?;
150 let read = file.read(remaining)?;
151 if read == 0 {
152 break;
153 }
154 filled = filled.saturating_add(read);
155 }
156 bytes.truncate(filled);
157 Ok(bytes)
158 }
159
160 fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
161 let mut file = OpenOptions::new()
162 .create(true)
163 .truncate(true)
164 .write(true)
165 .open(path)?;
166 file.write_all(bytes)
167 }
168
169 fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn VfsFile>> {
170 let file = OpenOptions::new().create(true).append(true).open(path)?;
171 Ok(Box::new(StdVfsFile(file)))
172 }
173
174 fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
175 std::fs::rename(from, to)
176 }
177
178 fn sync(&self, path: &Path, kind: SyncKind) -> std::io::Result<()> {
179 sync_file(&File::open(path)?, kind)
180 }
181
182 fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>> {
183 std::fs::read_dir(directory)?
184 .map(|entry| entry.map(|value| value.path()))
185 .collect()
186 }
187
188 fn delete(&self, path: &Path) -> std::io::Result<()> {
189 std::fs::remove_file(path)
190 }
191}
192
193pub struct CountingVfs<V> {
195 inner: V,
196 counters: Arc<CountingVfsCounters>,
197 read_path_filter: Option<PathBuf>,
198}
199
200#[derive(Default)]
201struct CountingVfsCounters {
202 open_calls: AtomicU64,
203 open_for_map_calls: AtomicU64,
204 read_calls: AtomicU64,
205 filtered_read_calls: AtomicU64,
206 read_bytes: AtomicU64,
207 segment_bytes_read: Arc<AtomicU64>,
208 write_calls: AtomicU64,
209 bytes_written: AtomicU64,
210 append_calls: AtomicU64,
211 bytes_appended: AtomicU64,
212 rename_calls: AtomicU64,
213 delete_calls: AtomicU64,
214 barrier_sync_calls: AtomicU64,
215 full_sync_calls: AtomicU64,
216 handle_barrier_sync_calls: AtomicU64,
217 handle_full_sync_calls: AtomicU64,
218}
219
220struct CountingVfsFile {
221 inner: Box<dyn VfsFile>,
222 counters: Arc<CountingVfsCounters>,
223}
224
225impl VfsFile for CountingVfsFile {
226 fn append(&mut self, bytes: &[u8]) -> std::io::Result<()> {
227 self.inner.append(bytes)?;
228 self.counters.append_calls.fetch_add(1, Ordering::Relaxed);
229 self.counters
230 .bytes_appended
231 .fetch_add(bytes.len() as u64, Ordering::Relaxed);
232 Ok(())
233 }
234
235 fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
236 let bytes = buffers
237 .iter()
238 .fold(0_usize, |total, buffer| total.saturating_add(buffer.len()));
239 self.inner.append_vectored(buffers)?;
240 self.counters.append_calls.fetch_add(1, Ordering::Relaxed);
241 self.counters
242 .bytes_appended
243 .fetch_add(bytes as u64, Ordering::Relaxed);
244 Ok(())
245 }
246
247 fn sync(&self, kind: SyncKind) -> std::io::Result<()> {
248 self.inner.sync(kind)?;
249 match kind {
250 SyncKind::Barrier => &self.counters.handle_barrier_sync_calls,
251 SyncKind::Full => &self.counters.handle_full_sync_calls,
252 }
253 .fetch_add(1, Ordering::Relaxed);
254 Ok(())
255 }
256}
257
258impl<V> CountingVfs<V> {
259 #[must_use]
261 pub fn new(inner: V) -> Self {
262 Self {
263 inner,
264 counters: Arc::new(CountingVfsCounters::default()),
265 read_path_filter: None,
266 }
267 }
268
269 #[must_use]
271 pub fn with_read_path_filter(mut self, path: impl Into<PathBuf>) -> Self {
272 self.read_path_filter = Some(path.into());
273 self
274 }
275
276 #[must_use]
278 pub const fn inner(&self) -> &V {
279 &self.inner
280 }
281
282 #[doc(hidden)]
284 pub fn audit_segment_data_reads<T>(&self, operation: impl FnOnce() -> T) -> T {
285 let _audit = crate::segment::reader::install_data_read_audit(Some(Arc::clone(
286 &self.counters.segment_bytes_read,
287 )));
288 operation()
289 }
290
291 #[must_use]
293 pub fn open_calls(&self) -> u64 {
294 self.counters.open_calls.load(Ordering::Relaxed)
295 }
296
297 #[must_use]
299 pub fn open_for_map_calls(&self) -> u64 {
300 self.counters.open_for_map_calls.load(Ordering::Relaxed)
301 }
302
303 #[must_use]
305 pub fn read_calls(&self) -> u64 {
306 self.counters.read_calls.load(Ordering::Relaxed)
307 }
308
309 #[must_use]
311 pub fn filtered_read_calls(&self) -> u64 {
312 self.counters.filtered_read_calls.load(Ordering::Relaxed)
313 }
314
315 #[must_use]
317 pub fn read_bytes(&self) -> u64 {
318 self.counters.read_bytes.load(Ordering::Relaxed)
319 }
320
321 #[must_use]
323 pub fn segment_bytes_read(&self) -> u64 {
324 self.counters.segment_bytes_read.load(Ordering::Relaxed)
325 }
326
327 #[must_use]
329 pub fn write_calls(&self) -> u64 {
330 self.counters.write_calls.load(Ordering::Relaxed)
331 }
332
333 #[must_use]
335 pub fn bytes_written(&self) -> u64 {
336 self.counters.bytes_written.load(Ordering::Relaxed)
337 }
338
339 #[must_use]
341 pub fn append_calls(&self) -> u64 {
342 self.counters.append_calls.load(Ordering::Relaxed)
343 }
344
345 #[must_use]
347 pub fn bytes_appended(&self) -> u64 {
348 self.counters.bytes_appended.load(Ordering::Relaxed)
349 }
350
351 #[must_use]
353 pub fn rename_calls(&self) -> u64 {
354 self.counters.rename_calls.load(Ordering::Relaxed)
355 }
356
357 #[must_use]
359 pub fn delete_calls(&self) -> u64 {
360 self.counters.delete_calls.load(Ordering::Relaxed)
361 }
362
363 #[must_use]
365 pub fn barrier_sync_calls(&self) -> u64 {
366 self.counters.barrier_sync_calls.load(Ordering::Relaxed)
367 }
368
369 #[must_use]
371 pub fn full_sync_calls(&self) -> u64 {
372 self.counters.full_sync_calls.load(Ordering::Relaxed)
373 }
374
375 #[must_use]
377 pub fn handle_barrier_sync_calls(&self) -> u64 {
378 self.counters
379 .handle_barrier_sync_calls
380 .load(Ordering::Relaxed)
381 }
382
383 #[must_use]
385 pub fn handle_full_sync_calls(&self) -> u64 {
386 self.counters.handle_full_sync_calls.load(Ordering::Relaxed)
387 }
388
389 pub fn reset(&self) {
391 self.counters.open_calls.store(0, Ordering::Relaxed);
392 self.counters.open_for_map_calls.store(0, Ordering::Relaxed);
393 self.counters.read_calls.store(0, Ordering::Relaxed);
394 self.counters
395 .filtered_read_calls
396 .store(0, Ordering::Relaxed);
397 self.counters.read_bytes.store(0, Ordering::Relaxed);
398 self.counters.segment_bytes_read.store(0, Ordering::Relaxed);
399 self.counters.write_calls.store(0, Ordering::Relaxed);
400 self.counters.bytes_written.store(0, Ordering::Relaxed);
401 self.counters.append_calls.store(0, Ordering::Relaxed);
402 self.counters.bytes_appended.store(0, Ordering::Relaxed);
403 self.counters.rename_calls.store(0, Ordering::Relaxed);
404 self.counters.delete_calls.store(0, Ordering::Relaxed);
405 self.counters.barrier_sync_calls.store(0, Ordering::Relaxed);
406 self.counters.full_sync_calls.store(0, Ordering::Relaxed);
407 self.counters
408 .handle_barrier_sync_calls
409 .store(0, Ordering::Relaxed);
410 self.counters
411 .handle_full_sync_calls
412 .store(0, Ordering::Relaxed);
413 }
414}
415
416impl<V: Vfs> Vfs for CountingVfs<V> {
417 fn segment_data_read_counter(&self) -> Option<Arc<AtomicU64>> {
418 Some(Arc::clone(&self.counters.segment_bytes_read))
419 }
420
421 fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool> {
422 self.inner.ensure_directory(path, create)
423 }
424
425 fn open(&self, path: &Path) -> std::io::Result<u64> {
426 self.counters.open_calls.fetch_add(1, Ordering::Relaxed);
427 self.inner.open(path)
428 }
429
430 fn open_for_map(&self, path: &Path) -> std::io::Result<File> {
431 self.counters
432 .open_for_map_calls
433 .fetch_add(1, Ordering::Relaxed);
434 self.inner.open_for_map(path)
435 }
436
437 fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
438 let bytes = self.inner.read(path)?;
439 self.counters.read_calls.fetch_add(1, Ordering::Relaxed);
440 if self.read_path_filter.as_deref() == Some(path) {
441 self.counters
442 .filtered_read_calls
443 .fetch_add(1, Ordering::Relaxed);
444 }
445 self.counters
446 .read_bytes
447 .fetch_add(bytes.len() as u64, Ordering::Relaxed);
448 if is_segment_path(path) {
449 self.counters
450 .segment_bytes_read
451 .fetch_add(bytes.len() as u64, Ordering::Relaxed);
452 }
453 Ok(bytes)
454 }
455
456 fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>> {
457 let bytes = self.inner.read_range(path, offset, length)?;
458 self.counters.read_calls.fetch_add(1, Ordering::Relaxed);
459 if self.read_path_filter.as_deref() == Some(path) {
460 self.counters
461 .filtered_read_calls
462 .fetch_add(1, Ordering::Relaxed);
463 }
464 self.counters
465 .read_bytes
466 .fetch_add(bytes.len() as u64, Ordering::Relaxed);
467 if is_segment_path(path) {
468 self.counters
469 .segment_bytes_read
470 .fetch_add(bytes.len() as u64, Ordering::Relaxed);
471 }
472 Ok(bytes)
473 }
474
475 fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
476 self.inner.write(path, bytes)?;
477 self.counters.write_calls.fetch_add(1, Ordering::Relaxed);
478 self.counters
479 .bytes_written
480 .fetch_add(bytes.len() as u64, Ordering::Relaxed);
481 Ok(())
482 }
483
484 fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn VfsFile>> {
485 Ok(Box::new(CountingVfsFile {
486 inner: self.inner.open_append(path)?,
487 counters: Arc::clone(&self.counters),
488 }))
489 }
490
491 fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
492 self.inner.rename(from, to)?;
493 self.counters.rename_calls.fetch_add(1, Ordering::Relaxed);
494 Ok(())
495 }
496
497 fn sync(&self, path: &Path, kind: SyncKind) -> std::io::Result<()> {
498 self.inner.sync(path, kind)?;
499 match kind {
500 SyncKind::Barrier => &self.counters.barrier_sync_calls,
501 SyncKind::Full => &self.counters.full_sync_calls,
502 }
503 .fetch_add(1, Ordering::Relaxed);
504 Ok(())
505 }
506
507 fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>> {
508 self.inner.list(directory)
509 }
510
511 fn delete(&self, path: &Path) -> std::io::Result<()> {
512 self.inner.delete(path)?;
513 self.counters.delete_calls.fetch_add(1, Ordering::Relaxed);
514 Ok(())
515 }
516}
517
518fn is_segment_path(path: &Path) -> bool {
519 path.file_name()
520 .and_then(|name| name.to_str())
521 .is_some_and(|name| name.starts_with("segment-") && name.ends_with(".zseg"))
522}
523
524#[cfg(any(target_os = "macos", target_os = "ios"))]
525fn sync_file(file: &File, kind: SyncKind) -> std::io::Result<()> {
526 let result = match kind {
527 SyncKind::Barrier => crate::sys::darwin::barrier_fsync(file.as_raw_fd()),
528 SyncKind::Full => crate::sys::darwin::full_fsync(file.as_raw_fd()),
529 };
530 result.map_err(std::io::Error::other)
531}
532
533#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
534fn sync_file(file: &File, kind: SyncKind) -> std::io::Result<()> {
535 let result = unsafe {
536 match kind {
538 SyncKind::Barrier => libc::fdatasync(file.as_raw_fd()),
539 SyncKind::Full => libc::fsync(file.as_raw_fd()),
540 }
541 };
542 if result == -1 {
543 Err(std::io::Error::last_os_error())
544 } else {
545 Ok(())
546 }
547}
548
549#[cfg(not(unix))]
550fn sync_file(_: &File, kind: SyncKind) -> std::io::Result<()> {
551 Err(std::io::Error::new(
552 std::io::ErrorKind::Unsupported,
553 format!("{kind:?} synchronization requires a supported file-descriptor platform"),
554 ))
555}
556
557#[cfg(test)]
558mod tests {
559 use std::path::Path;
560
561 use super::crash::MemoryVfs;
562 use super::{CountingVfs, SyncKind, Vfs};
563
564 #[test]
565 fn crash_support_api_gate_is_identical_in_debug_and_release() {
566 let source = include_str!("mod.rs");
567 let gate = source
568 .lines()
569 .zip(source.lines().skip(1))
570 .find_map(|(line, next)| (next == "pub mod crash;").then_some(line));
571 assert_eq!(gate, Some("#[cfg(any(test, feature = \"test-support\"))]"));
572 }
573
574 #[test]
575 #[allow(clippy::expect_used)]
576 fn counting_vfs_counts_every_mutation_and_sync_kind_exactly() {
577 let inner = MemoryVfs::new();
578 inner
579 .insert("/source", b"abc".to_vec())
580 .expect("seed source");
581 let counting = CountingVfs::new(inner);
582
583 assert_eq!(counting.open(Path::new("/source")).expect("open"), 3);
584 assert_eq!(counting.read(Path::new("/source")).expect("read"), b"abc");
585 assert_eq!(
586 counting
587 .read_range(Path::new("/source"), 1, 2)
588 .expect("range"),
589 b"bc"
590 );
591 counting.write(Path::new("/write"), b"1234").expect("write");
592 let mut handle = counting
593 .open_append(Path::new("/append"))
594 .expect("open append");
595 handle.append(b"12").expect("first append");
596 handle.append(b"345").expect("second append");
597 handle.sync(SyncKind::Barrier).expect("handle barrier");
598 handle.sync(SyncKind::Full).expect("handle full");
599 counting
600 .sync(Path::new("/write"), SyncKind::Barrier)
601 .expect("path barrier");
602 counting
603 .sync(Path::new("/write"), SyncKind::Full)
604 .expect("path full");
605 counting
606 .rename(Path::new("/write"), Path::new("/renamed"))
607 .expect("rename");
608 counting.delete(Path::new("/renamed")).expect("delete");
609
610 assert_eq!(
611 [
612 counting.open_calls(),
613 counting.read_calls(),
614 counting.read_bytes(),
615 counting.write_calls(),
616 counting.bytes_written(),
617 counting.append_calls(),
618 counting.bytes_appended(),
619 counting.rename_calls(),
620 counting.delete_calls(),
621 counting.barrier_sync_calls(),
622 counting.full_sync_calls(),
623 counting.handle_barrier_sync_calls(),
624 counting.handle_full_sync_calls(),
625 ],
626 [1, 2, 5, 1, 4, 2, 5, 1, 1, 1, 1, 1, 1],
627 "CountingVfs must expose every exact VFS and open-handle cost"
628 );
629 counting.reset();
630 assert_eq!(
631 [
632 counting.open_calls(),
633 counting.read_calls(),
634 counting.read_bytes(),
635 counting.write_calls(),
636 counting.bytes_written(),
637 counting.append_calls(),
638 counting.bytes_appended(),
639 counting.rename_calls(),
640 counting.delete_calls(),
641 counting.barrier_sync_calls(),
642 counting.full_sync_calls(),
643 counting.handle_barrier_sync_calls(),
644 counting.handle_full_sync_calls(),
645 ],
646 [0; 13],
647 "reset must zero decorator and shared-handle counters"
648 );
649 }
650}