use std::io::{self, SeekFrom};
use std::path::Path;
use std::pin::Pin;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::task::{Context, Poll};
use memmap2::Mmap;
use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncSeek, BufReader, ReadBuf};
use tokio::sync::Mutex;
use crate::Result;
pub(crate) enum BlockSource {
Buffered {
reader: BufReader<File>,
len: Option<u64>,
},
Mapped(MmapCursor),
#[cfg(unix)]
Direct(DirectCursor),
}
impl BlockSource {
#[cfg(test)]
pub(crate) fn buffered(file: File) -> Self {
BlockSource::Buffered {
reader: BufReader::new(file),
len: None,
}
}
pub(crate) fn buffered_sized(file: File, len: u64) -> Self {
BlockSource::Buffered {
reader: BufReader::new(file),
len: Some(len),
}
}
pub(crate) async fn len(&mut self) -> io::Result<u64> {
match self {
BlockSource::Buffered { reader, len } => match *len {
Some(l) => Ok(l),
None => {
let l = reader.get_ref().metadata().await?.len();
*len = Some(l);
Ok(l)
}
},
BlockSource::Mapped(c) => Ok(c.mmap.len() as u64),
#[cfg(unix)]
BlockSource::Direct(c) => Ok(c.len),
}
}
pub(crate) fn mapped(mmap: Arc<Mmap>) -> Self {
BlockSource::Mapped(MmapCursor::new(mmap))
}
#[cfg(unix)]
pub(crate) fn direct(cursor: DirectCursor) -> Self {
BlockSource::Direct(cursor)
}
#[cfg(test)]
pub(crate) fn is_mmap(&self) -> bool {
matches!(self, BlockSource::Mapped(_))
}
#[cfg(all(test, unix))]
pub(crate) fn is_direct(&self) -> bool {
matches!(self, BlockSource::Direct(_))
}
}
pub(crate) enum ScanSource {
Buffered {
file_len: u64,
},
Mapped(Arc<Mmap>),
#[cfg(unix)]
Direct {
window: usize,
file_len: u64,
},
}
impl ScanSource {
pub(crate) async fn open(&self, path: &Path) -> Result<BlockSource> {
use crate::storage::sstable::read_work_counters::record_file_open;
Ok(match self {
ScanSource::Buffered { file_len } => {
record_file_open();
BlockSource::buffered_sized(File::open(path).await?, *file_len)
}
ScanSource::Mapped(mmap) => BlockSource::mapped(mmap.clone()),
#[cfg(unix)]
ScanSource::Direct { window, file_len } => {
match DirectCursor::open(path, *window) {
Ok(cursor) => BlockSource::direct(cursor),
Err(e) => {
tracing::warn!(
"Direct-I/O reopen of {} for scan failed ({}); using buffered I/O",
path.display(),
e
);
record_file_open();
BlockSource::buffered_sized(File::open(path).await?, *file_len)
}
}
}
})
}
}
pub(crate) struct ScanCursor {
pub(crate) file: Arc<Mutex<BlockSource>>,
pub(crate) chunk_index: Arc<AtomicUsize>,
}
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<ScanCursor>() == 16);
impl ScanCursor {
pub(crate) fn new(source: BlockSource) -> Self {
Self {
file: Arc::new(Mutex::new(source)),
chunk_index: Arc::new(AtomicUsize::new(0)),
}
}
}
impl AsyncRead for BlockSource {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
match self.get_mut() {
BlockSource::Buffered { reader, .. } => Pin::new(reader).poll_read(cx, buf),
BlockSource::Mapped(c) => Pin::new(c).poll_read(cx, buf),
#[cfg(unix)]
BlockSource::Direct(c) => Pin::new(c).poll_read(cx, buf),
}
}
}
impl AsyncSeek for BlockSource {
fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
match self.get_mut() {
BlockSource::Buffered { reader, .. } => Pin::new(reader).start_seek(position),
BlockSource::Mapped(c) => Pin::new(c).start_seek(position),
#[cfg(unix)]
BlockSource::Direct(c) => Pin::new(c).start_seek(position),
}
}
fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
match self.get_mut() {
BlockSource::Buffered { reader, .. } => Pin::new(reader).poll_complete(cx),
BlockSource::Mapped(c) => Pin::new(c).poll_complete(cx),
#[cfg(unix)]
BlockSource::Direct(c) => Pin::new(c).poll_complete(cx),
}
}
}
pub(crate) struct MmapCursor {
mmap: Arc<Mmap>,
pos: u64,
}
impl MmapCursor {
fn new(mmap: Arc<Mmap>) -> Self {
Self { mmap, pos: 0 }
}
}
impl AsyncRead for MmapCursor {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.get_mut();
let data: &[u8] = &this.mmap;
let len = data.len() as u64;
if this.pos >= len {
return Poll::Ready(Ok(()));
}
let pos = this.pos as usize;
let remaining = &data[pos..];
let n = remaining.len().min(buf.remaining());
buf.put_slice(&remaining[..n]);
this.pos += n as u64;
Poll::Ready(Ok(()))
}
}
impl AsyncSeek for MmapCursor {
fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
let this = self.get_mut();
let len = this.mmap.len() as u64;
let new_pos = match position {
SeekFrom::Start(offset) => offset,
SeekFrom::End(offset) => offset_from(len, offset)?,
SeekFrom::Current(offset) => offset_from(this.pos, offset)?,
};
this.pos = new_pos;
Ok(())
}
fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
Poll::Ready(Ok(self.get_mut().pos))
}
}
fn offset_from(base: u64, offset: i64) -> io::Result<u64> {
let result = if offset >= 0 {
base.checked_add(offset as u64).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid seek to an overflowing position",
)
})?
} else {
base.checked_sub(offset.unsigned_abs()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid seek to a negative position",
)
})?
};
Ok(result)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) fn open_direct_file(path: &Path) -> io::Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECT)
.open(path)
}
#[cfg(target_os = "macos")]
pub(crate) fn open_direct_file(path: &Path) -> io::Result<std::fs::File> {
use std::os::unix::io::AsRawFd;
let file = std::fs::File::open(path)?;
let rc = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) };
if rc == -1 {
return Err(io::Error::last_os_error());
}
Ok(file)
}
#[cfg(all(
unix,
not(any(target_os = "linux", target_os = "android", target_os = "macos"))
))]
pub(crate) fn open_direct_file(_path: &Path) -> io::Result<std::fs::File> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"direct I/O is not supported on this platform",
))
}
#[cfg(unix)]
const DIRECT_IO_ALIGN: usize = 4096;
#[cfg(unix)]
pub(crate) struct DirectCursor {
file: std::fs::File,
len: u64,
pos: u64,
window: usize,
buf: AlignedBuf,
buf_off: u64,
buf_len: usize,
}
#[cfg(unix)]
impl DirectCursor {
pub(crate) fn open(path: &Path, window: usize) -> io::Result<Self> {
let file = Self::open_direct(path)?;
crate::storage::sstable::read_work_counters::record_file_open();
let len = file.metadata()?.len();
let align = DIRECT_IO_ALIGN;
let window = window
.max(align)
.checked_next_multiple_of(align)
.unwrap_or(usize::MAX & !(align - 1));
let buf = AlignedBuf::new(window, align)?;
Ok(Self {
file,
len,
pos: 0,
window,
buf,
buf_off: 0,
buf_len: 0,
})
}
fn open_direct(path: &Path) -> io::Result<std::fs::File> {
open_direct_file(path)
}
fn ensure_buffered(&mut self) -> io::Result<()> {
let covered = self.buf_len > 0
&& self.pos >= self.buf_off
&& self.pos < self.buf_off + self.buf_len as u64;
if covered {
return Ok(());
}
let align = DIRECT_IO_ALIGN as u64;
let aligned_off = (self.pos / align) * align;
let slice = self.buf.as_mut_slice();
debug_assert_eq!(slice.len(), self.window);
use std::os::unix::fs::FileExt;
let n = self.file.read_at(slice, aligned_off)?;
self.buf_off = aligned_off;
self.buf_len = n;
Ok(())
}
}
#[cfg(unix)]
impl AsyncRead for DirectCursor {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.get_mut();
if this.pos >= this.len {
return Poll::Ready(Ok(()));
}
if let Err(e) = this.ensure_buffered() {
return Poll::Ready(Err(e));
}
let rel = (this.pos - this.buf_off) as usize;
if rel >= this.buf_len {
return Poll::Ready(Ok(()));
}
let available = &this.buf.as_slice()[rel..this.buf_len];
let n = available.len().min(buf.remaining());
buf.put_slice(&available[..n]);
this.pos += n as u64;
Poll::Ready(Ok(()))
}
}
#[cfg(unix)]
impl AsyncSeek for DirectCursor {
fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
let this = self.get_mut();
this.pos = match position {
SeekFrom::Start(offset) => offset,
SeekFrom::End(offset) => offset_from(this.len, offset)?,
SeekFrom::Current(offset) => offset_from(this.pos, offset)?,
};
Ok(())
}
fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
Poll::Ready(Ok(self.get_mut().pos))
}
}
#[cfg(unix)]
struct AlignedBuf {
ptr: std::ptr::NonNull<u8>,
layout: std::alloc::Layout,
}
#[cfg(unix)]
unsafe impl Send for AlignedBuf {}
#[cfg(unix)]
impl AlignedBuf {
fn new(size: usize, align: usize) -> io::Result<Self> {
let size = size.max(align);
let layout = std::alloc::Layout::from_size_align(size, align)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let raw = unsafe { std::alloc::alloc_zeroed(layout) };
let ptr = std::ptr::NonNull::new(raw)
.ok_or_else(|| io::Error::new(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()) }
}
}
#[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 tokio::io::{AsyncReadExt, AsyncSeekExt};
fn cursor(bytes: &[u8]) -> MmapCursor {
let mut mmap = memmap2::MmapMut::map_anon(bytes.len().max(1)).unwrap();
mmap[..bytes.len()].copy_from_slice(bytes);
let mmap = mmap.make_read_only().unwrap();
MmapCursor::new(Arc::new(mmap))
}
#[tokio::test]
async fn reads_sequentially() {
let mut c = cursor(b"hello world");
let mut buf = [0u8; 5];
c.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"hello");
assert_eq!(c.stream_position().await.unwrap(), 5);
}
#[tokio::test]
async fn seek_start_current_end() {
let mut c = cursor(b"0123456789");
c.seek(SeekFrom::Start(3)).await.unwrap();
let mut b = [0u8; 2];
c.read_exact(&mut b).await.unwrap();
assert_eq!(&b, b"34");
c.seek(SeekFrom::Current(2)).await.unwrap();
c.read_exact(&mut b).await.unwrap();
assert_eq!(&b, b"78");
let end = c.seek(SeekFrom::End(0)).await.unwrap();
assert_eq!(end, 10);
}
#[tokio::test]
async fn read_past_eof_is_unexpected_eof() {
let mut c = cursor(b"abc");
c.seek(SeekFrom::Start(2)).await.unwrap();
let mut b = [0u8; 8];
let err = c.read_exact(&mut b).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
}
#[tokio::test]
async fn negative_seek_before_start_errors() {
let mut c = cursor(b"abc");
let err = c.seek(SeekFrom::Current(-5)).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn block_source_reports_backend() {
let mmap = memmap2::MmapMut::map_anon(8).unwrap();
let mmap = mmap.make_read_only().unwrap();
assert!(BlockSource::mapped(Arc::new(mmap)).is_mmap());
let dir = std::env::temp_dir();
let path = dir.join("cqlite_blocksource_backend_test.bin");
tokio::fs::write(&path, b"buffered").await.unwrap();
let file = tokio::fs::File::open(&path).await.unwrap();
assert!(!BlockSource::buffered(file).is_mmap());
tokio::fs::remove_file(&path).await.ok();
}
#[tokio::test]
async fn positive_seek_overflow_errors() {
let mut c = cursor(b"abc");
c.seek(SeekFrom::Start(u64::MAX)).await.unwrap();
let err = c.seek(SeekFrom::Current(1)).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let mut c2 = cursor(b"abc"); let landed = c2.seek(SeekFrom::End(i64::MAX)).await.unwrap();
assert_eq!(landed, 3u64 + i64::MAX as u64);
}
#[tokio::test]
async fn seek_past_eof_preserves_position_like_file() {
let mut c = cursor(b"abc"); let landed = c.seek(SeekFrom::Start(10)).await.unwrap();
assert_eq!(landed, 10);
let mut b = [0u8; 4];
let n = c.read(&mut b).await.unwrap();
assert_eq!(n, 0, "read past EOF yields no bytes");
assert_eq!(c.stream_position().await.unwrap(), 10);
c.seek(SeekFrom::Start(1)).await.unwrap();
let mut one = [0u8; 1];
c.read_exact(&mut one).await.unwrap();
assert_eq!(&one, b"b");
}
#[tokio::test]
async fn seek_past_eof_position_matches_real_file() {
let bytes = b"abc";
let dir = std::env::temp_dir();
let path = dir.join("cqlite_mmapcursor_eof_parity.bin");
tokio::fs::write(&path, bytes).await.unwrap();
let mut file = tokio::fs::File::open(&path).await.unwrap();
file.seek(SeekFrom::Start(10)).await.unwrap();
let mut fb = [0u8; 4];
let file_n = file.read(&mut fb).await.unwrap();
let file_pos = file.stream_position().await.unwrap();
tokio::fs::remove_file(&path).await.ok();
let mut c = cursor(bytes);
c.seek(SeekFrom::Start(10)).await.unwrap();
let mut cb = [0u8; 4];
let cur_n = c.read(&mut cb).await.unwrap();
let cur_pos = c.stream_position().await.unwrap();
assert_eq!(cur_n, file_n, "byte count parity");
assert_eq!(cur_pos, file_pos, "post-read position parity");
}
#[tokio::test]
async fn multipage_read_across_page_boundary() {
let len = 10_000usize;
let mut data = vec![0u8; len];
for (i, b) in data.iter_mut().enumerate() {
*b = (i % 251) as u8; }
let mut c = cursor(&data);
c.seek(SeekFrom::Start(4090)).await.unwrap();
let mut window = [0u8; 16]; c.read_exact(&mut window).await.unwrap();
for (k, b) in window.iter().enumerate() {
assert_eq!(*b, ((4090 + k) % 251) as u8);
}
assert_eq!(c.stream_position().await.unwrap(), 4106);
c.seek(SeekFrom::Start((len - 4) as u64)).await.unwrap();
let mut tail = [0u8; 4];
c.read_exact(&mut tail).await.unwrap();
for (k, b) in tail.iter().enumerate() {
assert_eq!(*b, ((len - 4 + k) % 251) as u8);
}
assert_eq!(c.stream_position().await.unwrap(), len as u64);
}
#[tokio::test]
async fn partial_read_returns_available_bytes() {
let mut c = cursor(b"abcd");
c.seek(SeekFrom::Start(2)).await.unwrap();
let mut b = [0u8; 8];
let n = c.read(&mut b).await.unwrap();
assert_eq!(n, 2);
assert_eq!(&b[..2], b"cd");
}
#[cfg(unix)]
#[tokio::test]
async fn direct_cursor_reads_and_seeks() {
use tokio::io::{AsyncReadExt, AsyncSeekExt};
let len = DIRECT_IO_ALIGN * 3 + 123;
let mut data = vec![0u8; len];
for (i, b) in data.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
let dir = std::env::temp_dir();
let path = dir.join(format!("cqlite_directcursor_{}.bin", std::process::id()));
tokio::fs::write(&path, &data).await.unwrap();
let mut cursor = match DirectCursor::open(&path, DIRECT_IO_ALIGN * 2) {
Ok(c) => c,
Err(e) => {
eprintln!("O_DIRECT unsupported here ({e}); skipping direct cursor test");
tokio::fs::remove_file(&path).await.ok();
return;
}
};
let mut got = Vec::with_capacity(len);
let mut chunk = [0u8; 1000];
loop {
let n = cursor.read(&mut chunk).await.unwrap();
if n == 0 {
break;
}
got.extend_from_slice(&chunk[..n]);
}
assert_eq!(got, data, "direct sequential read must match contents");
cursor.seek(SeekFrom::Start(4090)).await.unwrap();
let mut window = [0u8; 16];
cursor.read_exact(&mut window).await.unwrap();
for (k, b) in window.iter().enumerate() {
assert_eq!(*b, ((4090 + k) % 251) as u8, "byte {k} after seek");
}
cursor
.seek(SeekFrom::Start((len - 4) as u64))
.await
.unwrap();
let mut tail = [0u8; 8];
let err = cursor.read_exact(&mut tail).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
tokio::fs::remove_file(&path).await.ok();
}
}