metrale_storage/tier/direct_swap/
unix.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Unix `DirectSwapFile`: `pread`/`pwrite` on a file opened `O_DIRECT` on
4//! Linux, with an aligned bounce for unaligned buffers.
5//!
6//! Owner: storage, tiered-cache core.
7//! Invariants:
8//! - A read or write moves exactly `record_bytes`, or returns an error.
9
10use std::fs::OpenOptions;
11use std::os::fd::{AsRawFd, OwnedFd};
12use std::os::unix::fs::OpenOptionsExt;
13use std::path::Path;
14
15use anyhow::{Result, bail};
16
17use crate::tier::direct_swap::validate_record_bytes;
18use crate::tier::traits::SwapStore;
19
20/// 2026-09-25: `O_DIRECT` on Linux; other unix targets open the file buffered.
21#[cfg(target_os = "linux")]
22const DIRECT_FLAGS: i32 = libc::O_DIRECT;
23#[cfg(not(target_os = "linux"))]
24const DIRECT_FLAGS: i32 = 0;
25
26/// 2026-09-25: Fixed-stride swap file: record `disk_slot` at
27/// `disk_slot * record_bytes`, with `record_bytes` a non-zero 4 KiB multiple.
28///
29/// A buffer that is not 4 KiB-aligned is staged through an internal aligned bounce,
30/// one extra copy of the record. `Residency` passes its page-aligned scratch.
31///
32/// An empty swap file does not mean a write failed: `Residency` writes a record only
33/// when its arena has no free slot. The file then reaches
34/// `(highest disk_slot + 1) * record_bytes` and never shrinks, since this type keeps
35/// the no-op `discard_record` default.
36pub struct DirectSwapFile {
37    fd: OwnedFd,
38    record_bytes: usize,
39    /// 2026-09-25: Aligned bounce for callers whose buffer is not 4 KiB-aligned.
40    bounce: AlignedBuf,
41}
42
43impl DirectSwapFile {
44    pub fn create(path: &Path, record_bytes: usize) -> Result<Self> {
45        validate_record_bytes(record_bytes)?;
46        let f = OpenOptions::new()
47            .read(true)
48            .write(true)
49            .create(true)
50            .truncate(true)
51            .custom_flags(DIRECT_FLAGS)
52            .open(path)
53            .map_err(|e| anyhow::anyhow!("open O_DIRECT {}: {e}", path.display()))?;
54        Ok(Self {
55            fd: OwnedFd::from(f),
56            record_bytes,
57            bounce: AlignedBuf::new(record_bytes),
58        })
59    }
60
61    fn offset(&self, disk_slot: usize) -> libc::off_t {
62        (disk_slot as u64 * self.record_bytes as u64) as libc::off_t
63    }
64}
65
66impl SwapStore for DirectSwapFile {
67    fn record_bytes(&self) -> usize {
68        self.record_bytes
69    }
70
71    fn write_record(&mut self, disk_slot: usize, bytes: &[u8]) -> Result<()> {
72        if bytes.len() != self.record_bytes {
73            bail!(
74                "write_record: {} bytes, expected {}",
75                bytes.len(),
76                self.record_bytes
77            );
78        }
79        let off = self.offset(disk_slot);
80        let src = if is_aligned(bytes.as_ptr()) {
81            bytes.as_ptr()
82        } else {
83            self.bounce.as_mut_slice().copy_from_slice(bytes);
84            self.bounce.ptr()
85        };
86        let n = unsafe {
87            libc::pwrite(
88                self.fd.as_raw_fd(),
89                src as *const libc::c_void,
90                self.record_bytes,
91                off,
92            )
93        };
94        if n != self.record_bytes as isize {
95            bail!("pwrite record {disk_slot} returned {n}, errno {}", errno());
96        }
97        Ok(())
98    }
99
100    fn read_record(&self, disk_slot: usize, out: &mut [u8]) -> Result<()> {
101        if out.len() != self.record_bytes {
102            bail!(
103                "read_record: {} bytes, expected {}",
104                out.len(),
105                self.record_bytes
106            );
107        }
108        let off = self.offset(disk_slot);
109        if is_aligned(out.as_ptr()) {
110            let n = unsafe {
111                libc::pread(
112                    self.fd.as_raw_fd(),
113                    out.as_mut_ptr() as *mut libc::c_void,
114                    self.record_bytes,
115                    off,
116                )
117            };
118            if n != self.record_bytes as isize {
119                bail!("pread record {disk_slot} returned {n}, errno {}", errno());
120            }
121        } else {
122            // 2026-09-25: Stage through the bounce, written through a raw pointer from
123            // `&self`. `AlignedBuf` is not `Sync`, so neither is this type, and no two
124            // `read_record` calls run at once.
125            let bp = self.bounce.ptr();
126            let n = unsafe {
127                libc::pread(
128                    self.fd.as_raw_fd(),
129                    bp as *mut libc::c_void,
130                    self.record_bytes,
131                    off,
132                )
133            };
134            if n != self.record_bytes as isize {
135                bail!(
136                    "pread(bounce) record {disk_slot} returned {n}, errno {}",
137                    errno()
138                );
139            }
140            unsafe {
141                std::ptr::copy_nonoverlapping(bp, out.as_mut_ptr(), self.record_bytes);
142            }
143        }
144        Ok(())
145    }
146}
147
148fn errno() -> i32 {
149    std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
150}
151
152fn is_aligned(p: *const u8) -> bool {
153    (p as usize) & 0xfff == 0
154}
155
156/// 2026-09-25: A 4 KiB-aligned heap buffer from `posix_memalign`, for O_DIRECT
157/// staging; a failed allocation panics.
158struct AlignedBuf {
159    ptr: *mut u8,
160    len: usize,
161}
162unsafe impl Send for AlignedBuf {}
163impl AlignedBuf {
164    fn new(len: usize) -> Self {
165        let mut p: *mut libc::c_void = std::ptr::null_mut();
166        let rc = unsafe { libc::posix_memalign(&mut p, 4096, len) };
167        assert!(
168            rc == 0 && !p.is_null(),
169            "posix_memalign({len}) failed rc={rc}"
170        );
171        Self {
172            ptr: p as *mut u8,
173            len,
174        }
175    }
176    fn ptr(&self) -> *mut u8 {
177        self.ptr
178    }
179    fn as_mut_slice(&mut self) -> &mut [u8] {
180        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
181    }
182}
183impl Drop for AlignedBuf {
184    fn drop(&mut self) {
185        unsafe { libc::free(self.ptr as *mut libc::c_void) }
186    }
187}