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_path(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(not(windows))]
532fn sync_path(path: &Path, kind: SyncKind) -> std::io::Result<()> {
533 sync_file(&File::open(path)?, kind)
534}
535
536#[cfg(windows)]
537fn sync_path(path: &Path, _kind: SyncKind) -> std::io::Result<()> {
538 crate::sys::windows::sync_path(path)
542}
543
544#[cfg(any(target_os = "macos", target_os = "ios"))]
545fn sync_file(file: &File, kind: SyncKind) -> std::io::Result<()> {
546 let result = match kind {
547 SyncKind::Barrier => crate::sys::darwin::barrier_fsync(file.as_raw_fd()),
548 SyncKind::Full => crate::sys::darwin::full_fsync(file.as_raw_fd()),
549 };
550 result.map_err(std::io::Error::other)
551}
552
553#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
554fn sync_file(file: &File, kind: SyncKind) -> std::io::Result<()> {
555 let result = unsafe {
556 match kind {
558 SyncKind::Barrier => libc::fdatasync(file.as_raw_fd()),
559 SyncKind::Full => libc::fsync(file.as_raw_fd()),
560 }
561 };
562 if result == -1 {
563 Err(std::io::Error::last_os_error())
564 } else {
565 Ok(())
566 }
567}
568
569#[cfg(windows)]
575fn sync_file(file: &File, _kind: SyncKind) -> std::io::Result<()> {
576 crate::sys::windows::flush_file(file)
577}
578
579#[cfg(not(any(unix, windows)))]
580fn sync_file(_: &File, kind: SyncKind) -> std::io::Result<()> {
581 Err(std::io::Error::new(
582 std::io::ErrorKind::Unsupported,
583 format!("{kind:?} synchronization requires a supported file-descriptor platform"),
584 ))
585}
586
587#[cfg(test)]
588mod tests {
589 use std::path::Path;
590
591 use super::crash::MemoryVfs;
592 use super::{CountingVfs, SyncKind, Vfs};
593
594 #[test]
595 fn crash_support_api_gate_is_identical_in_debug_and_release() {
596 let source = include_str!("mod.rs");
597 let gate = source
598 .lines()
599 .zip(source.lines().skip(1))
600 .find_map(|(line, next)| (next == "pub mod crash;").then_some(line));
601 assert_eq!(gate, Some("#[cfg(any(test, feature = \"test-support\"))]"));
602 }
603
604 #[test]
605 #[allow(clippy::expect_used)]
606 fn counting_vfs_counts_every_mutation_and_sync_kind_exactly() {
607 let inner = MemoryVfs::new();
608 inner
609 .insert("/source", b"abc".to_vec())
610 .expect("seed source");
611 let counting = CountingVfs::new(inner);
612
613 assert_eq!(counting.open(Path::new("/source")).expect("open"), 3);
614 assert_eq!(counting.read(Path::new("/source")).expect("read"), b"abc");
615 assert_eq!(
616 counting
617 .read_range(Path::new("/source"), 1, 2)
618 .expect("range"),
619 b"bc"
620 );
621 counting.write(Path::new("/write"), b"1234").expect("write");
622 let mut handle = counting
623 .open_append(Path::new("/append"))
624 .expect("open append");
625 handle.append(b"12").expect("first append");
626 handle.append(b"345").expect("second append");
627 handle.sync(SyncKind::Barrier).expect("handle barrier");
628 handle.sync(SyncKind::Full).expect("handle full");
629 counting
630 .sync(Path::new("/write"), SyncKind::Barrier)
631 .expect("path barrier");
632 counting
633 .sync(Path::new("/write"), SyncKind::Full)
634 .expect("path full");
635 counting
636 .rename(Path::new("/write"), Path::new("/renamed"))
637 .expect("rename");
638 counting.delete(Path::new("/renamed")).expect("delete");
639
640 assert_eq!(
641 [
642 counting.open_calls(),
643 counting.read_calls(),
644 counting.read_bytes(),
645 counting.write_calls(),
646 counting.bytes_written(),
647 counting.append_calls(),
648 counting.bytes_appended(),
649 counting.rename_calls(),
650 counting.delete_calls(),
651 counting.barrier_sync_calls(),
652 counting.full_sync_calls(),
653 counting.handle_barrier_sync_calls(),
654 counting.handle_full_sync_calls(),
655 ],
656 [1, 2, 5, 1, 4, 2, 5, 1, 1, 1, 1, 1, 1],
657 "CountingVfs must expose every exact VFS and open-handle cost"
658 );
659 counting.reset();
660 assert_eq!(
661 [
662 counting.open_calls(),
663 counting.read_calls(),
664 counting.read_bytes(),
665 counting.write_calls(),
666 counting.bytes_written(),
667 counting.append_calls(),
668 counting.bytes_appended(),
669 counting.rename_calls(),
670 counting.delete_calls(),
671 counting.barrier_sync_calls(),
672 counting.full_sync_calls(),
673 counting.handle_barrier_sync_calls(),
674 counting.handle_full_sync_calls(),
675 ],
676 [0; 13],
677 "reset must zero decorator and shared-handle counters"
678 );
679 }
680}