use core::task::Waker;
use std::io;
use std::sync::Arc;
use super::Allocate;
use super::call::{End, FileOutcome, LentReadEnd, LentWriteEnd, Slot, SlotJob, WriteCall};
use super::file::{SyncKind, sync_on};
use crate::buf::PoolBuf;
use crate::buf::owned::OwnedBuf;
use crate::io::source::requests::Lease;
use crate::io::sys;
use crate::rt::counters::tally;
use crate::syscore::linux::iouring::ring::Sqe;
pub(crate) enum FileMem {
Read { buf: PoolBuf, from: u32, len: u32 },
ReadLent(OwnedBuf),
Write { buf: PoolBuf, done: usize },
WriteLent { buf: OwnedBuf, done: usize },
Call(Call),
Shutdown(libc::c_int),
}
pub(crate) enum Call {
Sync { data_only: bool },
Allocate { len: u64, mode: Allocate },
SyncRange { len: u32 },
}
const SYNC_RANGE_FLAGS: u32 = 1 | 2 | 4;
pub(crate) struct FileOp {
fd: Lease,
at: u64,
mem: FileMem,
slot: Arc<Slot>,
write: Option<WriteCall>,
}
#[inline]
fn submit<B, T, M>(
fd: &Lease,
at: u64,
buf: B,
mem: M,
waker: Option<&Waker>,
) -> Result<SlotJob<T>, B>
where
T: End,
M: FnOnce(B) -> FileMem,
{
crate::rt::context::current(|current| {
let Some((shard, ring)) = current.and_then(|shard| Some((shard, shard.ring()?))) else {
return Err(buf);
};
Ok(issue(shard, ring, fd, at, buf, mem, waker))
})
}
#[inline]
fn submit_held<B, T, M>(
fd: &Lease,
at: u64,
held: &mut Option<B>,
mem: M,
waker: Option<&Waker>,
) -> Option<SlotJob<T>>
where
T: End,
M: FnOnce(B) -> FileMem,
{
crate::rt::context::current(|current| {
let (shard, ring) = current.and_then(|shard| Some((shard, shard.ring()?)))?;
let buf = held.take()?;
Some(issue(shard, ring, fd, at, buf, mem, waker))
})
}
#[inline(always)]
fn issue<B, T, M>(
shard: &crate::rt::shard::Shard,
ring: &crate::rt::shard::ring::ShardRing,
fd: &Lease,
at: u64,
buf: B,
mem: M,
waker: Option<&Waker>,
) -> SlotJob<T>
where
T: End,
M: FnOnce(B) -> FileMem,
{
let slot = Slot::new();
if let Some(waker) = waker {
slot.register(waker);
}
let mem = mem(buf);
let write = matches!(
mem,
FileMem::Write { .. } | FileMem::WriteLent { .. } | FileMem::Call(Call::Allocate { .. })
)
.then(|| WriteCall::on(&shard.shared));
ring.submit_file(FileOp {
fd: fd.clone(),
at,
mem,
slot: Arc::clone(&slot),
write,
});
SlotJob::new(slot)
}
#[inline]
pub(crate) fn read(
fd: &Lease,
at: u64,
buf: PoolBuf,
from: usize,
len: usize,
) -> Result<SlotJob<io::Result<PoolBuf>>, PoolBuf> {
submit(
fd,
at,
(buf, from, len),
|(buf, from, len)| FileMem::Read {
buf,
from: u32::try_from(from).unwrap_or(u32::MAX),
len: u32::try_from(len).unwrap_or(u32::MAX),
},
None,
)
.map_err(|(buf, _, _)| buf)
}
pub(crate) fn sync(fd: &Lease, data_only: bool) -> Result<SlotJob<io::Result<()>>, ()> {
submit(
fd,
0,
data_only,
|data_only| FileMem::Call(Call::Sync { data_only }),
None,
)
.map_err(drop)
}
pub(crate) fn allocate(
fd: &Lease,
offset: u64,
len: u64,
mode: Allocate,
) -> Result<SlotJob<io::Result<()>>, ()> {
submit(
fd,
offset,
(len, mode),
|(len, mode)| FileMem::Call(Call::Allocate { len, mode }),
None,
)
.map_err(drop)
}
pub(crate) fn sync_range(fd: &Lease, offset: u64, len: u32) -> Result<SlotJob<io::Result<()>>, ()> {
submit(
fd,
offset,
len,
|len| FileMem::Call(Call::SyncRange { len }),
None,
)
.map_err(drop)
}
#[inline]
pub(crate) fn read_lent(
fd: &Lease,
at: u64,
held: &mut Option<OwnedBuf>,
waker: &Waker,
) -> Option<SlotJob<LentReadEnd>> {
submit_held(fd, at, held, FileMem::ReadLent, Some(waker))
}
#[inline]
pub(crate) fn write(fd: &Lease, at: u64, buf: PoolBuf) -> Result<SlotJob<io::Result<()>>, PoolBuf> {
submit(fd, at, buf, |buf| FileMem::Write { buf, done: 0 }, None)
}
pub(crate) fn shutdown(
fd: &Lease,
how: libc::c_int,
) -> Result<SlotJob<io::Result<()>>, libc::c_int> {
submit(fd, 0, how, FileMem::Shutdown, None)
}
#[inline]
pub(crate) fn write_lent(
fd: &Lease,
at: u64,
buf: OwnedBuf,
) -> Result<SlotJob<LentWriteEnd>, OwnedBuf> {
submit(fd, at, buf, |buf| FileMem::WriteLent { buf, done: 0 }, None)
}
fn len32(len: usize) -> u32 {
u32::try_from(len).unwrap_or(u32::MAX)
}
impl FileOp {
pub(crate) fn entry(&mut self) -> Sqe {
let fd = self.fd.raw();
match &mut self.mem {
FileMem::Read { buf, from, len } => {
let spare = buf.spare();
let room = spare.get_mut(*from as usize..).unwrap_or_default();
let len = (*len as usize).min(room.len());
Sqe::read(fd, room.as_mut_ptr(), len32(len), self.at)
}
FileMem::ReadLent(OwnedBuf::Vec(vec)) => {
let spare = vec.spare_capacity_mut();
Sqe::read(fd, spare.as_mut_ptr().cast(), len32(spare.len()), self.at)
}
FileMem::ReadLent(buf) => other_read_entry(fd, buf, self.at),
FileMem::Write { buf, done } => {
let bytes = buf.filled().get(*done..).unwrap_or_default();
Sqe::write(
fd,
bytes.as_ptr(),
len32(bytes.len()),
self.at + *done as u64,
)
}
FileMem::WriteLent { buf, done } => {
if let Some(bufs) = buf.vectored_mut() {
return vectored_entry(fd, bufs, Some(*done), self.at);
}
let bytes = buf.rest_from(*done);
Sqe::write(
fd,
bytes.as_ptr(),
len32(bytes.len()),
self.at + *done as u64,
)
}
FileMem::Call(_) | FileMem::Shutdown(_) => self.call_entry(fd),
}
}
#[cold]
#[inline(never)]
fn call_entry(&self, fd: std::os::fd::RawFd) -> Sqe {
match &self.mem {
FileMem::Call(Call::Sync { data_only }) => Sqe::fsync(fd, *data_only),
FileMem::Call(Call::Allocate { len, mode }) => {
Sqe::fallocate(fd, self.at, *len, sys::fallocate_mode(*mode) as u32)
}
FileMem::Call(Call::SyncRange { len }) => {
Sqe::sync_file_range(fd, self.at, *len, SYNC_RANGE_FLAGS)
}
FileMem::Shutdown(how) => Sqe::shutdown(fd, *how),
_ => Sqe::nop(),
}
}
pub(crate) fn complete(mut self, res: i32) -> Option<FileOp> {
if tally::counting() {
self.tally(res);
}
if res == -libc::ECANCELED {
self.finish_on_pool();
return None;
}
if let (FileMem::Write { buf, done }, Ok(n @ 1..)) = (&mut self.mem, usize::try_from(res))
&& *done + n < buf.filled().len()
{
*done += n;
return Some(self);
}
if let (FileMem::WriteLent { buf, done }, Ok(n @ 1..)) =
(&mut self.mem, usize::try_from(res))
&& *done + n < buf.len()
{
*done += n;
return Some(self);
}
let result = match res {
r if r < 0 => Err(io::Error::from_raw_os_error(-r)),
r => Ok(r as usize),
};
let outcome = match self.mem {
FileMem::Read { mut buf, from, .. } => FileOutcome::Read(result.map(|n| {
buf.set_filled_at(from as usize, n);
buf
})),
FileMem::ReadLent(mut buf) => {
if let Ok(n) = result {
unsafe { buf.add_len(n) };
}
FileOutcome::ReadLent(result, buf)
}
FileMem::Write { .. } => FileOutcome::Write(written(result)),
FileMem::WriteLent { buf, .. } => FileOutcome::WriteLent(written(result), buf),
FileMem::Call(Call::Sync { .. } | Call::SyncRange { .. }) => {
FileOutcome::Sync(result.map(drop))
}
FileMem::Call(Call::Allocate { .. }) => FileOutcome::Write(result.map(drop)),
FileMem::Shutdown(_) => FileOutcome::Write(result.map(drop)),
};
self.slot.put(outcome);
drop(self.write.take());
None
}
#[cold]
#[inline(never)]
fn tally(&self, res: i32) {
let Ok(n) = usize::try_from(res) else {
return;
};
match self.mem {
FileMem::Read { .. } | FileMem::ReadLent(_) => tally::add_read(n),
FileMem::Write { .. } | FileMem::WriteLent { .. } if n > 0 => tally::add_write(n),
_ => {}
}
}
pub(crate) fn give_up(&mut self) {
self.write = None;
}
fn finish_on_pool(self) {
let FileOp {
fd,
at,
mem,
slot,
write,
..
} = self;
let call = move || {
let raw = fd.raw();
let outcome = match mem {
FileMem::Read { mut buf, from, len } => {
let (from, len) = (from as usize, len as usize);
let spare = buf.spare();
let end = from.saturating_add(len).min(spare.len());
let room = spare.get_mut(from..end).unwrap_or_default();
FileOutcome::Read(sys::pread(raw, room, at).map(|n| {
buf.set_filled_at(from, n);
buf
}))
}
FileMem::ReadLent(mut buf) => {
let result = crate::fs::file::pread_owned(raw, &mut buf, at);
FileOutcome::ReadLent(result, buf)
}
FileMem::Write { buf, done } => FileOutcome::Write(sys::pwrite_all(
raw,
buf.filled().get(done..).unwrap_or_default(),
at + done as u64,
)),
FileMem::WriteLent { mut buf, done } => {
let result =
crate::fs::file::pwrite_owned(raw, &mut buf, done, at + done as u64);
FileOutcome::WriteLent(result, buf)
}
FileMem::Call(Call::Sync { data_only }) => FileOutcome::Sync(sync_on(
&fd,
if data_only {
SyncKind::Data
} else {
SyncKind::All
},
)),
FileMem::Call(Call::SyncRange { len }) => FileOutcome::Sync(sync_on(
&fd,
SyncKind::Range {
offset: at,
len: u64::from(len),
},
)),
FileMem::Call(Call::Allocate { len, mode }) => {
FileOutcome::Write(sys::allocate(raw, at, len, mode))
}
FileMem::Shutdown(how) => FileOutcome::Write(sys::shutdown_raw(raw, how)),
};
crate::fs::call::Ending::new(slot, outcome, write)
};
match crate::rt::runtime::blocking_pool_inside() {
Some(pool) => pool.spawn(call).detach(),
None => drop(call()),
}
}
}
#[inline(never)]
fn other_read_entry(fd: std::os::fd::RawFd, buf: &mut OwnedBuf, at: u64) -> Sqe {
if let Some(bufs) = buf.vectored_mut() {
return vectored_entry(fd, bufs, None, at);
}
let spare = buf.spare_capacity_mut();
Sqe::read(fd, spare.as_mut_ptr().cast(), len32(spare.len()), at)
}
#[inline(never)]
fn vectored_entry(
fd: std::os::fd::RawFd,
bufs: &mut crate::buf::vectored::Vectored,
done: Option<usize>,
at: u64,
) -> Sqe {
match done {
None => {
let iov = bufs.read_iovecs();
Sqe::readv(fd, iov.as_ptr(), len32(iov.len()), at)
}
Some(done) => {
let iov = bufs.write_iovecs(done);
Sqe::writev(fd, iov.as_ptr(), len32(iov.len()), at + done as u64)
}
}
}
fn written(result: io::Result<usize>) -> io::Result<()> {
match result {
Ok(0) => Err(io::ErrorKind::WriteZero.into()),
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
#[cfg(test)]
mod tests {
use std::io::{Read, Seek, SeekFrom, Write};
use std::time::Duration;
use super::*;
fn temp_file(tag: &str) -> (std::fs::File, std::path::PathBuf) {
let path = std::env::temp_dir().join(format!(
"nuclei-fs-ring-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)
.unwrap();
(file, path)
}
fn op_of(file: &std::fs::File, at: u64, mem: FileMem) -> (FileOp, Arc<Slot>) {
use std::os::fd::AsRawFd;
let slot = Slot::new();
let source = crate::io::Source::new(file.as_raw_fd(), crate::io::source::Kind::File);
(
FileOp {
fd: source.lease().unwrap(),
at,
mem,
slot: Arc::clone(&slot),
write: None,
},
slot,
)
}
#[test]
fn a_short_write_goes_on_with_the_rest() {
let (file, path) = temp_file("short");
let (op, slot) = op_of(
&file,
10,
FileMem::WriteLent {
buf: OwnedBuf::Vec(b"0123456789".to_vec()),
done: 0,
},
);
let mut rest = op.complete(4).expect("the rest of the write goes on");
match &rest.mem {
FileMem::WriteLent { done, .. } => assert_eq!(*done, 4),
_ => panic!("the write became another call"),
}
assert!(slot.take().is_none(), "the write ended early");
let entry = rest.entry();
assert_eq!((entry.raw().off, entry.raw().len), (14, 6));
assert!(rest.complete(6).is_none(), "the whole write landed");
match slot.take() {
Some(FileOutcome::WriteLent(Ok(()), buf)) => assert_eq!(&*buf, b"0123456789"),
_ => panic!("the write did not end whole"),
}
std::fs::remove_file(path).unwrap();
}
#[test]
fn a_call_cancelled_as_its_shard_went_is_made_on_the_pool() {
let (mut file, path) = temp_file("cancelled");
let (op, slot) = op_of(
&file,
0,
FileMem::WriteLent {
buf: OwnedBuf::Vec(b"landed".to_vec()),
done: 0,
},
);
assert!(op.complete(-libc::ECANCELED).is_none());
let deadline = std::time::Instant::now() + Duration::from_secs(5);
let outcome = loop {
if let Some(outcome) = slot.take() {
break outcome;
}
assert!(
std::time::Instant::now() < deadline,
"the pool never made the call"
);
std::thread::sleep(Duration::from_millis(1));
};
assert!(matches!(outcome, FileOutcome::WriteLent(Ok(()), _)));
let mut got = String::new();
file.seek(SeekFrom::Start(0)).unwrap();
file.read_to_string(&mut got).unwrap();
assert_eq!(got, "landed");
file.write_all(b"").unwrap();
std::fs::remove_file(path).unwrap();
}
#[test]
fn a_file_is_read_and_written_through_the_ring() {
use crate::rt::backend::Backend;
use crate::rt::context;
use crate::rt::shard::{Limits, Shard};
use crate::syscore::linux::iouring::ring::RingConfig;
let shard = match Shard::new(Limits {
max_nesting: 8,
buffer_cap: crate::buf::DEFAULT_CAP,
backend: Backend::Ring(RingConfig::new(64)),
}) {
Ok(shard) => shard,
Err(err) => {
eprintln!("skipping: io_uring unavailable: {err}");
return;
}
};
let _current = context::enter(&shard);
let (file, path) = temp_file("through");
let data: Vec<u8> = (0..(3 << 20)).map(|i| (i * 7 + i / 251) as u8).collect();
let got = shard.block_on(async {
let mut handle = crate::io::Handle::new(file).unwrap();
let mut rest = &data[..];
while !rest.is_empty() {
let n = core::future::poll_fn(|cx| {
futures_io::AsyncWrite::poll_write(core::pin::Pin::new(&mut handle), cx, rest)
})
.await
.unwrap();
rest = &rest[n..];
}
core::future::poll_fn(|cx| {
futures_io::AsyncWrite::poll_flush(core::pin::Pin::new(&mut handle), cx)
})
.await
.unwrap();
core::future::poll_fn(|cx| {
futures_io::AsyncSeek::poll_seek(
core::pin::Pin::new(&mut handle),
cx,
SeekFrom::Start(0),
)
})
.await
.unwrap();
let mut got = Vec::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = core::future::poll_fn(|cx| {
futures_io::AsyncRead::poll_read(core::pin::Pin::new(&mut handle), cx, &mut buf)
})
.await
.unwrap();
if n == 0 {
break got;
}
got.extend_from_slice(&buf[..n]);
}
});
assert!(got == data, "the file's bytes came back changed");
assert!(
shard.ring().unwrap().file_calls() > 0,
"no call of the file went to the ring"
);
std::fs::remove_file(path).unwrap();
}
}