#![allow(unsafe_code)]
use std::os::unix::io::AsRawFd;
use std::sync::Arc;
use io_uring::IoUring;
use io_uring::opcode;
use io_uring::types::Fd;
use parking_lot::Mutex;
use crate::Result;
use crate::errors::PagedbError;
use crate::vfs::blocking::offload;
use crate::vfs::traits::{
VfsFile, checked_indexed_completion, checked_iouring_positioned_offset, checked_read_count,
checked_signed_file_len, write_all_at,
};
use crate::vfs::types::{ReadReq, WriteReq};
pub struct IouringFile {
file: Arc<std::fs::File>,
writable: bool,
ring: Arc<Mutex<IoUring>>,
}
impl IouringFile {
pub(crate) fn new(file: std::fs::File, writable: bool, ring: Arc<Mutex<IoUring>>) -> Self {
Self {
file: Arc::new(file),
writable,
ring,
}
}
fn shared(&self) -> (Arc<std::fs::File>, Arc<Mutex<IoUring>>) {
(Arc::clone(&self.file), Arc::clone(&self.ring))
}
fn check_write_range(offset: u64, len: usize) -> Result<()> {
let len = u64::try_from(len).map_err(|_| {
PagedbError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"buffer length does not fit in u64",
))
})?;
offset.checked_add(len).ok_or_else(|| {
PagedbError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"write offset overflow",
))
})?;
Ok(())
}
fn entry_len(len: usize) -> Result<u32> {
u32::try_from(len)
.map_err(|_| PagedbError::Io(std::io::Error::other("buffer too large for u32")))
}
unsafe fn submit_one(
ring: &mut IoUring,
entry: &io_uring::squeue::Entry,
user_data: u64,
) -> std::io::Result<i32> {
unsafe {
ring.submission()
.push(entry)
.map_err(|_| std::io::Error::other("submission queue full"))?;
}
ring.submit_and_wait(1)?;
let mut result = None;
{
let mut cq = ring.completion();
cq.sync();
for cqe in cq.by_ref() {
if cqe.user_data() == user_data {
result = Some(cqe.result());
break;
}
}
}
let res =
result.ok_or_else(|| std::io::Error::other("io_uring: expected CQE not found"))?;
if res < 0 {
Err(std::io::Error::from_raw_os_error(-res))
} else {
Ok(res)
}
}
unsafe fn submit_batch(
ring: &mut IoUring,
entries: &[io_uring::squeue::Entry],
) -> std::io::Result<Vec<i32>> {
let total = entries.len();
if total == 0 {
return Ok(Vec::new());
}
let chunk_size = crate::vfs::iouring::ring::RING_DEPTH as usize;
let mut results = vec![0i32; total];
let mut base = 0usize;
while base < total {
let end = (base + chunk_size).min(total);
let chunk_len = end - base;
{
let mut sq = ring.submission();
for (i, entry) in entries[base..end].iter().enumerate() {
let tagged = entry.clone().user_data(i as u64);
unsafe {
sq.push(&tagged)
.map_err(|_| std::io::Error::other("submission queue full"))?;
}
}
}
ring.submit_and_wait(chunk_len)?;
let mut chunk_results = vec![None; chunk_len];
let mut found = 0usize;
{
let mut cq = ring.completion();
cq.sync();
for cqe in cq.by_ref() {
if checked_indexed_completion(&mut chunk_results, cqe.user_data(), cqe.result())
.map_err(|error| match error {
PagedbError::Io(io) => io,
other => std::io::Error::other(other.to_string()),
})?
{
found += 1;
}
if found == chunk_len {
break;
}
}
}
if found < chunk_len {
return Err(std::io::Error::other(
"io_uring: fewer CQEs returned than submitted",
));
}
for (index, result) in chunk_results.into_iter().enumerate() {
results[base + index] = result
.ok_or_else(|| std::io::Error::other("io_uring: missing indexed CQE result"))?;
}
base = end;
}
Ok(results)
}
}
impl VfsFile for IouringFile {
async fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let len = buf.len();
checked_iouring_positioned_offset(offset, len)?;
Self::entry_len(len)?;
let (file, ring) = self.shared();
let (scratch, read) = offload(move || {
let fd = Fd(file.as_raw_fd());
let entry_len = IouringFile::entry_len(len)?;
let mut scratch = vec![0u8; len];
let entry = opcode::Read::new(fd, scratch.as_mut_ptr(), entry_len)
.offset(offset)
.build()
.user_data(0);
let n = {
let mut guard = ring.lock();
unsafe { IouringFile::submit_one(&mut guard, &entry, 0) }
}
.map_err(PagedbError::Io)?;
#[allow(clippy::cast_sign_loss)]
let read = checked_read_count(n as usize, len)?;
Ok((scratch, read))
})
.await?;
buf[..read].copy_from_slice(&scratch[..read]);
Ok(read)
}
async fn read_at_vectored(&self, reqs: &mut [ReadReq<'_>]) -> Result<()> {
if reqs.is_empty() {
return Ok(());
}
let mut plan: Vec<(u64, usize)> = Vec::with_capacity(reqs.len());
for req in reqs.iter() {
checked_iouring_positioned_offset(req.offset, req.buf.len())?;
Self::entry_len(req.buf.len())?;
plan.push((req.offset, req.buf.len()));
}
let (file, ring) = self.shared();
let completed = offload(move || {
let fd = Fd(file.as_raw_fd());
let mut buffers: Vec<Vec<u8>> = plan.iter().map(|&(_, len)| vec![0u8; len]).collect();
let mut entries: Vec<io_uring::squeue::Entry> = Vec::with_capacity(plan.len());
for (i, ((offset, len), scratch)) in plan.iter().zip(buffers.iter_mut()).enumerate() {
let entry_len = IouringFile::entry_len(*len)?;
entries.push(
opcode::Read::new(fd, scratch.as_mut_ptr(), entry_len)
.offset(*offset)
.build()
.user_data(i as u64),
);
}
let results = {
let mut guard = ring.lock();
unsafe { IouringFile::submit_batch(&mut guard, &entries) }
}
.map_err(PagedbError::Io)?;
drop(entries);
let mut out: Vec<(Vec<u8>, usize)> = Vec::with_capacity(plan.len());
for ((_, len), (scratch, res)) in plan.iter().zip(buffers.into_iter().zip(results)) {
if res < 0 {
return Err(PagedbError::Io(std::io::Error::from_raw_os_error(-res)));
}
#[allow(clippy::cast_sign_loss)]
let nread = checked_read_count(res as usize, *len)?;
out.push((scratch, nread));
}
Ok(out)
})
.await?;
for (req, (scratch, nread)) in reqs.iter_mut().zip(completed) {
req.buf[..nread].copy_from_slice(&scratch[..nread]);
for b in &mut req.buf[nread..] {
*b = 0;
}
}
Ok(())
}
async fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<usize> {
if !self.writable {
return Err(PagedbError::ReadOnly);
}
if buf.is_empty() {
return Ok(0);
}
Self::check_write_range(offset, buf.len())?;
Self::entry_len(buf.len())?;
let (file, ring) = self.shared();
let data = buf.to_vec();
offload(move || {
let fd = Fd(file.as_raw_fd());
let entry_len = IouringFile::entry_len(data.len())?;
let entry = opcode::Write::new(fd, data.as_ptr(), entry_len)
.offset(offset)
.build()
.user_data(0);
let n = {
let mut guard = ring.lock();
unsafe { IouringFile::submit_one(&mut guard, &entry, 0) }
}
.map_err(PagedbError::Io)?;
#[allow(clippy::cast_sign_loss)]
let written = n as usize;
Ok(written)
})
.await
}
async fn write_at_vectored(&mut self, reqs: &[WriteReq<'_>]) -> Result<()> {
if !self.writable {
return Err(PagedbError::ReadOnly);
}
if reqs.is_empty() {
return Ok(());
}
for req in reqs {
Self::check_write_range(req.offset, req.buf.len())?;
}
let mut plan: Vec<(usize, u64, Vec<u8>)> = Vec::with_capacity(reqs.len());
for (i, req) in reqs.iter().enumerate() {
if req.buf.is_empty() {
continue;
}
Self::entry_len(req.buf.len())?;
plan.push((i, req.offset, req.buf.to_vec()));
}
if plan.is_empty() {
return Ok(());
}
let (file, ring) = self.shared();
let short_writes = offload(move || {
let fd = Fd(file.as_raw_fd());
let mut entries: Vec<io_uring::squeue::Entry> = Vec::with_capacity(plan.len());
for (slot, (_, offset, data)) in plan.iter().enumerate() {
let entry_len = IouringFile::entry_len(data.len())?;
let user_data = u64::try_from(slot).map_err(|_| {
PagedbError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"too many vectored write requests",
))
})?;
entries.push(
opcode::Write::new(fd, data.as_ptr(), entry_len)
.offset(*offset)
.build()
.user_data(user_data),
);
}
let results = {
let mut guard = ring.lock();
unsafe { IouringFile::submit_batch(&mut guard, &entries) }
}
.map_err(PagedbError::Io)?;
drop(entries);
let mut short_writes = Vec::new();
for (slot, &res) in results.iter().enumerate() {
if res < 0 {
return Err(PagedbError::Io(std::io::Error::from_raw_os_error(-res)));
}
let written = usize::try_from(res)
.map_err(|_| PagedbError::Io(std::io::Error::other("negative write result")))?;
let (request_index, _, data) = &plan[slot];
if written > data.len() {
return Err(PagedbError::Io(std::io::Error::other(
"io_uring write overreported bytes",
)));
}
if written == 0 {
return Err(PagedbError::Io(std::io::Error::from(
std::io::ErrorKind::WriteZero,
)));
}
if written < data.len() {
short_writes.push((*request_index, written));
}
}
Ok(short_writes)
})
.await?;
for (request_index, written) in short_writes {
let request = &reqs[request_index];
let written_u64 = u64::try_from(written).map_err(|_| {
PagedbError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"write count does not fit in u64",
))
})?;
let offset = request.offset.checked_add(written_u64).ok_or_else(|| {
PagedbError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"write offset overflow",
))
})?;
write_all_at(self, offset, &request.buf[written..]).await?;
}
Ok(())
}
async fn sync(&mut self) -> Result<()> {
let (file, ring) = self.shared();
offload(move || {
let fd = Fd(file.as_raw_fd());
let entry = opcode::Fsync::new(fd).build().user_data(0);
let completed = {
let mut guard = ring.lock();
unsafe { IouringFile::submit_one(&mut guard, &entry, 0) }
};
completed.map_err(PagedbError::Io)?;
Ok(())
})
.await
}
async fn truncate(&mut self, len: u64) -> Result<()> {
if !self.writable {
return Err(PagedbError::ReadOnly);
}
let len = checked_signed_file_len(len, "ftruncate")?;
let file = Arc::clone(&self.file);
offload(move || {
let rc = unsafe { libc::ftruncate(file.as_raw_fd(), len) };
if rc != 0 {
return Err(PagedbError::Io(std::io::Error::last_os_error()));
}
Ok(())
})
.await
}
async fn len(&self) -> Result<u64> {
let file = Arc::clone(&self.file);
offload(move || Ok(file.metadata().map_err(PagedbError::Io)?.len())).await
}
async fn is_empty(&self) -> Result<bool> {
Ok(self.len().await? == 0)
}
fn supports_direct_io(&self) -> bool {
true
}
}