use std::sync::Arc;
use memmap2::Mmap;
use crate::{Error, Result};
pub(crate) trait ReadAt: Send + Sync {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize>;
fn len(&self) -> u64;
fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
let mut filled = 0usize;
while filled < buf.len() {
let n = self.read_at(offset + filled as u64, &mut buf[filled..])?;
if n == 0 {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"read_exact_at: reached EOF after {filled} of {} bytes at offset 0x{offset:x}",
buf.len()
),
)));
}
filled += n;
}
Ok(())
}
fn read_exact_at_reusing(
&self,
offset: u64,
buf: &mut [u8],
_scratch: &mut DirectScratch,
) -> Result<()> {
self.read_exact_at(offset, buf)
}
}
pub(crate) struct DirectScratch {
#[cfg(unix)]
bounce: Option<AlignedBuf>,
}
impl DirectScratch {
pub(crate) fn new() -> Self {
Self {
#[cfg(unix)]
bounce: None,
}
}
}
impl<T: ReadAt + ?Sized> ReadAt for Arc<T> {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
(**self).read_at(offset, buf)
}
fn len(&self) -> u64 {
(**self).len()
}
fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
(**self).read_exact_at(offset, buf)
}
fn read_exact_at_reusing(
&self,
offset: u64,
buf: &mut [u8],
scratch: &mut DirectScratch,
) -> Result<()> {
(**self).read_exact_at_reusing(offset, buf, scratch)
}
}
#[cfg(test)]
pub(crate) struct SleepingReadAt<R: ReadAt> {
inner: R,
delay: std::time::Duration,
calls: Arc<std::sync::atomic::AtomicUsize>,
}
#[cfg(test)]
impl<R: ReadAt> SleepingReadAt<R> {
pub(crate) fn new(
inner: R,
delay: std::time::Duration,
calls: Arc<std::sync::atomic::AtomicUsize>,
) -> Self {
Self {
inner,
delay,
calls,
}
}
}
#[cfg(test)]
impl<R: ReadAt> ReadAt for SleepingReadAt<R> {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
self.calls
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
std::thread::sleep(self.delay);
self.inner.read_at(offset, buf)
}
fn len(&self) -> u64 {
self.inner.len()
}
}
#[cfg(test)]
pub(crate) struct SerializingReadAt<R: ReadAt> {
inner: R,
delay: std::time::Duration,
lock: std::sync::Mutex<()>,
}
#[cfg(test)]
impl<R: ReadAt> SerializingReadAt<R> {
pub(crate) fn new(inner: R, delay: std::time::Duration) -> Self {
Self {
inner,
delay,
lock: std::sync::Mutex::new(()),
}
}
}
#[cfg(test)]
impl<R: ReadAt> ReadAt for SerializingReadAt<R> {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
std::thread::sleep(self.delay);
self.inner.read_at(offset, buf)
}
fn len(&self) -> u64 {
self.inner.len()
}
}
pub(crate) struct PlainFileReadAt {
#[cfg(unix)]
file: Arc<std::fs::File>,
#[cfg(windows)]
file: std::sync::Mutex<std::fs::File>,
len: u64,
}
impl PlainFileReadAt {
pub(crate) fn new(file: std::fs::File, len: u64) -> Self {
Self {
#[cfg(unix)]
file: Arc::new(file),
#[cfg(windows)]
file: std::sync::Mutex::new(file),
len,
}
}
pub(crate) fn open(path: &std::path::Path, len: u64) -> Result<Self> {
crate::storage::sstable::read_work_counters::record_file_open();
let file = std::fs::File::open(path)?;
Ok(Self::new(file, len))
}
}
impl ReadAt for PlainFileReadAt {
#[cfg(unix)]
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
use std::os::unix::fs::FileExt;
Ok(self.file.read_at(buf, offset)?)
}
#[cfg(windows)]
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
use std::os::windows::fs::FileExt;
let file = self.file.lock().unwrap_or_else(|e| e.into_inner());
Ok(file.seek_read(buf, offset)?)
}
fn len(&self) -> u64 {
self.len
}
}
pub(crate) struct MmapReadAt {
mmap: Arc<Mmap>,
}
impl MmapReadAt {
pub(crate) fn new(mmap: Arc<Mmap>) -> Self {
Self { mmap }
}
}
impl ReadAt for MmapReadAt {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
let data: &[u8] = &self.mmap;
let len = data.len() as u64;
if offset >= len {
return Ok(0);
}
let start = offset as usize;
let avail = &data[start..];
let n = avail.len().min(buf.len());
buf[..n].copy_from_slice(&avail[..n]);
Ok(n)
}
fn len(&self) -> u64 {
self.mmap.len() as u64
}
}
#[cfg(unix)]
const DIRECT_IO_ALIGN: usize = 4096;
#[cfg(unix)]
pub(crate) struct DirectReadAt {
file: Arc<std::fs::File>,
len: u64,
}
#[cfg(unix)]
impl DirectReadAt {
pub(crate) fn open(path: &std::path::Path, len: u64) -> Result<Self> {
let file = super::source::open_direct_file(path)?;
crate::storage::sstable::read_work_counters::record_file_open();
Ok(Self {
file: Arc::new(file),
len,
})
}
#[cfg(test)]
pub(crate) fn from_plain_fd_for_test(file: std::fs::File, len: u64) -> Self {
Self {
file: Arc::new(file),
len,
}
}
}
#[cfg(unix)]
impl DirectReadAt {
fn read_at_into(
&self,
offset: u64,
buf: &mut [u8],
bounce: &mut Option<AlignedBuf>,
) -> Result<usize> {
use std::os::unix::fs::FileExt;
if offset >= self.len || buf.is_empty() {
return Ok(0);
}
let align = DIRECT_IO_ALIGN as u64;
let aligned_off = (offset / align) * align;
let head = (offset - aligned_off) as usize;
let want_end = offset
.checked_add(buf.len() as u64)
.ok_or_else(|| Error::corruption("direct read_at: offset+len overflow"))?
.min(self.len);
let span = (want_end - aligned_off) as usize;
let aligned_len = span
.checked_next_multiple_of(DIRECT_IO_ALIGN)
.unwrap_or(usize::MAX & !(DIRECT_IO_ALIGN - 1));
let need_alloc = bounce
.as_ref()
.map(|b| b.capacity() < aligned_len)
.unwrap_or(true);
if need_alloc {
let old_cap = bounce.as_ref().map(AlignedBuf::capacity).unwrap_or(0);
let new_cap = aligned_len.max(old_cap.saturating_mul(2));
*bounce = Some(AlignedBuf::new(new_cap, DIRECT_IO_ALIGN)?);
}
let slot = match bounce.as_mut() {
Some(b) => b,
None => return Err(Error::corruption("direct read_at: bounce buffer missing")),
};
let got = self
.file
.read_at(&mut slot.as_mut_slice()[..aligned_len], aligned_off)?;
if got <= head {
return Ok(0);
}
let usable = &slot.as_slice()[head..got];
let n = usable.len().min(buf.len());
buf[..n].copy_from_slice(&usable[..n]);
Ok(n)
}
}
#[cfg(unix)]
impl ReadAt for DirectReadAt {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
let mut bounce: Option<AlignedBuf> = None;
self.read_at_into(offset, buf, &mut bounce)
}
fn len(&self) -> u64 {
self.len
}
fn read_exact_at_reusing(
&self,
offset: u64,
buf: &mut [u8],
scratch: &mut DirectScratch,
) -> Result<()> {
let mut filled = 0usize;
while filled < buf.len() {
let n = self.read_at_into(
offset + filled as u64,
&mut buf[filled..],
&mut scratch.bounce,
)?;
if n == 0 {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"read_exact_at_reusing: reached EOF after {filled} of {} bytes at \
offset 0x{offset:x}",
buf.len()
),
)));
}
filled += n;
}
Ok(())
}
}
#[cfg(unix)]
struct AlignedBuf {
ptr: std::ptr::NonNull<u8>,
layout: std::alloc::Layout,
}
#[cfg(unix)]
unsafe impl Send for AlignedBuf {}
#[cfg(all(test, unix))]
pub(crate) static ALIGNED_BUF_ALLOCS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(unix)]
impl AlignedBuf {
fn new(size: usize, align: usize) -> Result<Self> {
#[cfg(test)]
ALIGNED_BUF_ALLOCS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let size = size.max(align);
let layout = std::alloc::Layout::from_size_align(size, align)
.map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)))?;
let raw = unsafe { std::alloc::alloc_zeroed(layout) };
let ptr = std::ptr::NonNull::new(raw).ok_or_else(|| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::OutOfMemory,
"aligned alloc failed",
))
})?;
Ok(Self { ptr, layout })
}
fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.layout.size()) }
}
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.layout.size()) }
}
fn capacity(&self) -> usize {
self.layout.size()
}
}
#[cfg(unix)]
impl Drop for AlignedBuf {
fn drop(&mut self) {
unsafe { std::alloc::dealloc(self.ptr.as_ptr(), self.layout) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Barrier;
fn temp_file(bytes: &[u8], tag: &str) -> std::path::PathBuf {
let path =
std::env::temp_dir().join(format!("cqlite_readat_{}_{}.bin", tag, std::process::id()));
std::fs::write(&path, bytes).unwrap();
path
}
#[test]
fn plain_file_positioned_reads_return_their_own_bytes() {
let data: Vec<u8> = (0..=255u8).cycle().take(9000).collect();
let path = temp_file(&data, "plain");
let src = PlainFileReadAt::open(&path, data.len() as u64).unwrap();
let mut a = [0u8; 8];
src.read_exact_at(100, &mut a).unwrap();
assert_eq!(&a, &data[100..108]);
let mut b = [0u8; 8];
src.read_exact_at(5000, &mut b).unwrap();
assert_eq!(&b, &data[5000..5008]);
std::fs::remove_file(&path).ok();
}
#[test]
fn mmap_positioned_reads_return_their_own_bytes() {
let data: Vec<u8> = (0..=255u8).cycle().take(9000).collect();
let path = temp_file(&data, "mmap");
let file = std::fs::File::open(&path).unwrap();
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
let src = MmapReadAt::new(Arc::new(mmap));
let mut a = [0u8; 8];
src.read_exact_at(100, &mut a).unwrap();
assert_eq!(&a, &data[100..108]);
let mut b = [0u8; 8];
src.read_exact_at(5000, &mut b).unwrap();
assert_eq!(&b, &data[5000..5008]);
std::fs::remove_file(&path).ok();
}
#[test]
fn interleaved_concurrent_reads_do_not_cross_contaminate() {
let data: Vec<u8> = (0..=255u8).cycle().take(1 << 16).collect();
let path = temp_file(&data, "interleave");
let src = Arc::new(PlainFileReadAt::open(&path, data.len() as u64).unwrap());
let barrier = Arc::new(Barrier::new(2));
let off_a = 111u64;
let off_b = 40000u64;
let expect_a = data[off_a as usize..off_a as usize + 32].to_vec();
let expect_b = data[off_b as usize..off_b as usize + 32].to_vec();
let (s1, b1) = (Arc::clone(&src), Arc::clone(&barrier));
let t = std::thread::spawn(move || {
let mut acc = Vec::new();
b1.wait();
for _ in 0..200 {
let mut buf = [0u8; 32];
s1.read_exact_at(off_a, &mut buf).unwrap();
acc.push(buf.to_vec());
}
acc
});
barrier.wait();
for _ in 0..200 {
let mut buf = [0u8; 32];
src.read_exact_at(off_b, &mut buf).unwrap();
assert_eq!(buf.to_vec(), expect_b, "offset B read got A's/other bytes");
}
for got in t.join().unwrap() {
assert_eq!(got, expect_a, "offset A read got B's/other bytes");
}
std::fs::remove_file(&path).ok();
}
#[test]
fn read_exact_at_past_eof_is_unexpected_eof() {
let data = b"abc";
let path = temp_file(data, "eof");
let src = PlainFileReadAt::open(&path, data.len() as u64).unwrap();
let mut buf = [0u8; 8];
let err = src.read_exact_at(1, &mut buf).unwrap_err();
match err {
Error::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof),
other => panic!("expected UnexpectedEof, got {other:?}"),
}
std::fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
#[serial_test::serial(aligned_buf_allocs)]
fn direct_read_at_matches_plain_oracle_at_boundaries() {
let len = DIRECT_IO_ALIGN * 2 + 1234;
let mut data = vec![0u8; len];
let mut x = 0x1234_5678u32;
for b in data.iter_mut() {
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
*b = (x >> 24) as u8;
}
let path = temp_file(&data, "direct");
let oracle = PlainFileReadAt::open(&path, data.len() as u64).unwrap();
let direct = match DirectReadAt::open(&path, data.len() as u64) {
Ok(d) => d,
Err(e) => {
eprintln!("O_DIRECT open unsupported here ({e}); skipping DirectReadAt test");
std::fs::remove_file(&path).ok();
return;
}
};
let mut probe = [0u8; 16];
if let Err(e) = direct.read_at(0, &mut probe) {
eprintln!("O_DIRECT read unsupported here ({e}); skipping DirectReadAt test");
std::fs::remove_file(&path).ok();
return;
}
let align = DIRECT_IO_ALIGN as u64;
let cases: &[(u64, usize)] = &[
(0, 100), (37, 200), (align - 10, 50), (align, DIRECT_IO_ALIGN), (align + 7, 5000), ((len - 300) as u64, 300), ((len - 10) as u64, 5), ];
for &(off, n) in cases {
let mut want = vec![0u8; n];
oracle.read_exact_at(off, &mut want).unwrap();
let mut got = vec![0u8; n];
direct.read_exact_at(off, &mut got).unwrap();
assert_eq!(got, want, "DirectReadAt mismatch at off={off} n={n}");
}
let tail_off = (len - 100) as u64;
let mut big = vec![0u8; 500];
let got_n = direct.read_at(tail_off, &mut big).unwrap();
let mut oracle_big = vec![0u8; 500];
let oracle_n = oracle.read_at(tail_off, &mut oracle_big).unwrap();
assert_eq!(got_n, oracle_n, "short-read length must match oracle");
assert_eq!(got_n, 100, "should read exactly the 100 remaining bytes");
assert_eq!(&big[..got_n], &oracle_big[..oracle_n]);
let mut none = [0u8; 32];
assert_eq!(direct.read_at(len as u64, &mut none).unwrap(), 0);
assert_eq!(direct.read_at(len as u64 + align, &mut none).unwrap(), 0);
let err = direct.read_exact_at(len as u64, &mut none).unwrap_err();
match err {
Error::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof),
other => panic!("expected UnexpectedEof, got {other:?}"),
}
std::fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
#[serial_test::serial(aligned_buf_allocs)]
fn direct_windowed_read_reuses_one_bounce_buffer_across_chunks() {
use std::sync::atomic::Ordering;
fn fill(data: &mut [u8], mut x: u32) {
for b in data.iter_mut() {
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
*b = (x >> 24) as u8;
}
}
let chunk = DIRECT_IO_ALIGN; let n_chunks = 8usize;
let len = chunk * n_chunks;
let mut data = vec![0u8; len];
fill(&mut data, 0x9e37_79b9);
let path = temp_file(&data, "reuse");
let file = std::fs::File::open(&path).unwrap();
let direct = DirectReadAt::from_plain_fd_for_test(file, len as u64);
ALIGNED_BUF_ALLOCS.store(0, Ordering::Relaxed);
let mut scratch = DirectScratch::new();
for i in 0..n_chunks {
let off = (i * chunk) as u64;
let mut got = vec![0u8; chunk];
direct
.read_exact_at_reusing(off, &mut got, &mut scratch)
.unwrap();
assert_eq!(
got,
&data[i * chunk..(i + 1) * chunk],
"reused-buffer read returned wrong bytes for chunk {i}"
);
}
let reuse_allocs = ALIGNED_BUF_ALLOCS.load(Ordering::Relaxed);
assert_eq!(
reuse_allocs, 1,
"#2319: the Direct windowed-scan path must allocate ONE aligned bounce buffer \
and reuse it across all {n_chunks} chunk reads (got {reuse_allocs} allocs)"
);
ALIGNED_BUF_ALLOCS.store(0, Ordering::Relaxed);
for i in 0..n_chunks {
let off = (i * chunk) as u64;
let mut got = vec![0u8; chunk];
direct.read_at(off, &mut got).unwrap();
}
let percall_allocs = ALIGNED_BUF_ALLOCS.load(Ordering::Relaxed);
assert_eq!(
percall_allocs, n_chunks,
"#2319 (regression witness): the per-call point-read path allocates one aligned \
buffer PER read ({n_chunks} reads -> {n_chunks} allocs); the windowed scan must \
NOT use it (got {percall_allocs})"
);
assert!(
reuse_allocs < percall_allocs,
"#2319: reuse ({reuse_allocs}) must allocate strictly fewer buffers than the \
per-chunk path ({percall_allocs})"
);
std::fs::remove_file(&path).ok();
let n_reads = 8usize;
let grow_len = chunk * n_reads; let mut grow_data = vec![0u8; grow_len];
fill(&mut grow_data, 0x1357_9bdf);
let grow_path = temp_file(&grow_data, "reuse_grow");
let grow_file = std::fs::File::open(&grow_path).unwrap();
let grow_direct = DirectReadAt::from_plain_fd_for_test(grow_file, grow_len as u64);
ALIGNED_BUF_ALLOCS.store(0, Ordering::Relaxed);
let mut grow_scratch = DirectScratch::new();
for i in 0..n_reads {
let want_len = (i + 1) * chunk; let mut got = vec![0u8; want_len];
grow_direct
.read_exact_at_reusing(0, &mut got, &mut grow_scratch)
.unwrap();
assert_eq!(
got,
&grow_data[..want_len],
"increasing-size reused-buffer read returned wrong bytes at read {i}"
);
}
let grow_allocs = ALIGNED_BUF_ALLOCS.load(Ordering::Relaxed);
assert!(
grow_allocs <= 5,
"#2319: geometric growth must bound reallocations to a small constant across \
{n_reads} increasing-size reads (got {grow_allocs} allocs)"
);
assert!(
grow_allocs < n_reads,
"#2319: growth must allocate strictly fewer times than {n_reads} reads -- \
grow-to-exact reallocated on every size increase (got {grow_allocs})"
);
std::fs::remove_file(&grow_path).ok();
let arc_chunks = 6usize;
let arc_len = chunk * arc_chunks;
let mut arc_data = vec![0u8; arc_len];
fill(&mut arc_data, 0x2468_ace0);
let arc_path = temp_file(&arc_data, "reuse_arc");
let arc_file = std::fs::File::open(&arc_path).unwrap();
let src: Arc<dyn ReadAt> = Arc::new(DirectReadAt::from_plain_fd_for_test(
arc_file,
arc_len as u64,
));
ALIGNED_BUF_ALLOCS.store(0, Ordering::Relaxed);
let mut arc_scratch = DirectScratch::new();
for i in 0..arc_chunks {
let off = (i * chunk) as u64;
let mut got = vec![0u8; chunk];
src.read_exact_at_reusing(off, &mut got, &mut arc_scratch)
.unwrap();
assert_eq!(
got,
&arc_data[i * chunk..(i + 1) * chunk],
"Arc-dyn reused-buffer read returned wrong bytes for chunk {i}"
);
}
let arc_allocs = ALIGNED_BUF_ALLOCS.load(Ordering::Relaxed);
assert_eq!(
arc_allocs, 1,
"#2319: the Arc<dyn ReadAt> forward must reach DirectReadAt's reuse override -- \
exactly ONE aligned buffer across {arc_chunks} equal-sized chunk reads (got \
{arc_allocs}); a dropped forward would fall back to the per-chunk-alloc default"
);
std::fs::remove_file(&arc_path).ok();
}
}