use super::{
sockaddr::SockAddr,
waiter::{WaiterId, WaiterState},
};
use crate::{BLOB_PAGE_SIZE, Error, IoBuf, IoBufMut, IoBufs, storage::iouring::Shared};
use bytes::Buf as _;
use commonware_utils::{NZUsize, sync::AsyncMutex};
use io_uring::{opcode, squeue::Entry as SqueueEntry, types::Fd};
use std::{
fs::File,
net::TcpListener,
os::fd::{AsRawFd, OwnedFd, RawFd},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Instant,
};
pub const IOVEC_BATCH_SIZE: usize = 1024;
pub(crate) const DIRECT_ALIGN: usize = BLOB_PAGE_SIZE as usize;
const DIRECT_CHUNK: usize = i32::MAX as usize / DIRECT_ALIGN * DIRECT_ALIGN;
pub enum WriteBuffers {
Single {
buf: IoBuf,
offset: usize,
},
Vectored {
bufs: IoBufs,
chunk: usize,
offset: usize,
remaining: usize,
iovecs: Box<[libc::iovec]>,
},
}
unsafe impl Send for WriteBuffers {}
impl From<IoBufs> for WriteBuffers {
fn from(bufs: IoBufs) -> Self {
match bufs.try_into_single() {
Ok(buf) => Self::Single { buf, offset: 0 },
Err(bufs) => {
let max_iovecs = bufs.chunk_count().min(IOVEC_BATCH_SIZE);
let iovecs: Box<[libc::iovec]> = std::iter::repeat_n(
libc::iovec {
iov_base: std::ptr::NonNull::<u8>::dangling().as_ptr().cast(),
iov_len: 0,
},
max_iovecs,
)
.collect();
Self::Vectored {
remaining: bufs.len(),
bufs,
chunk: 0,
offset: 0,
iovecs,
}
}
}
}
}
impl WriteBuffers {
fn remaining_len(&self) -> usize {
match self {
Self::Single { buf, offset } => buf.len() - offset,
Self::Vectored { remaining, .. } => *remaining,
}
}
fn is_complete(&self) -> bool {
self.remaining_len() == 0
}
fn advance(&mut self, mut n: usize) {
assert!(
n <= self.remaining_len(),
"write CQE exceeds remaining bytes"
);
match self {
Self::Single { offset, .. } => *offset += n,
Self::Vectored {
bufs,
chunk,
offset,
remaining,
..
} => {
*remaining -= n;
while n > 0 {
let len = bufs.chunk_at(*chunk).expect("missing write chunk").len() - *offset;
if n < len {
*offset += n;
break;
}
n -= len;
*chunk += 1;
*offset = 0;
}
}
}
}
}
fn fill_iovecs(bufs: &IoBufs, chunk: usize, offset: usize, iovecs: &mut [libc::iovec]) -> u32 {
let mut count = 0;
for (index, iovec) in iovecs.iter_mut().enumerate() {
let Some(bytes) = bufs.chunk_at(chunk + index) else {
break;
};
let bytes = if index == 0 { &bytes[offset..] } else { bytes };
*iovec = libc::iovec {
iov_base: bytes.as_ptr().cast_mut().cast(),
iov_len: bytes.len(),
};
count += 1;
}
count
}
pub enum Request {
Send(SendRequest),
Recv(RecvRequest),
ReadAt(ReadAtRequest),
WriteAt(WriteAtRequest),
Sync(SyncRequest),
Connect(ConnectRequest),
Poll(PollRequest),
}
impl Request {
pub const fn deadline(&self) -> Option<Instant> {
match self {
Self::Send(r) => r.deadline,
Self::Recv(r) => r.deadline,
Self::Connect(r) => r.deadline,
Self::Poll(r) => r.deadline,
Self::ReadAt(_) | Self::WriteAt(_) | Self::Sync(_) => None,
}
}
pub const fn retains_on_orphan(&self) -> bool {
matches!(self, Self::WriteAt(_) | Self::Sync(_))
}
pub fn durability(&self) -> Option<&Arc<AsyncMutex<()>>> {
match self {
Self::Sync(r) => Some(&r.file.durability),
Self::WriteAt(r) if r.state != WriteAtState::Writing => Some(&r.file.durability),
_ => None,
}
}
pub fn prepare_durability(&self) -> Option<Result<(), Error>> {
match self {
Self::Sync(r) => match r.file.needs_sync() {
Ok(true) => None,
Ok(false) => Some(Ok(())),
Err(error) => Some(Err(error)),
},
Self::WriteAt(r) => r.file.tracker.failure().map(Err),
_ => unreachable!("durability permit for another request kind"),
}
}
pub fn build_sqe(&mut self, waiter_id: WaiterId) -> SqueueEntry {
let sqe = match self {
Self::Send(s) => s.build_sqe(),
Self::Recv(r) => r.build_sqe(),
Self::ReadAt(r) => r.build_sqe(),
Self::WriteAt(w) => w.build_sqe(),
Self::Sync(s) => s.build_sqe(),
Self::Connect(r) => r.build_sqe(),
Self::Poll(r) => r.build_sqe(),
};
sqe.user_data(waiter_id.user_data())
}
pub fn on_cqe(&mut self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
match self {
Self::Send(s) => s.on_cqe(state, result),
Self::Recv(r) => r.on_cqe(state, result),
Self::ReadAt(r) => r.on_cqe(state, result),
Self::WriteAt(w) => w.on_cqe(state, result),
Self::Sync(s) => s.on_cqe(state, result),
Self::Connect(r) => r.on_cqe(state, result),
Self::Poll(r) => r.on_cqe(state, result),
}
}
pub fn complete(self, result: Result<(), Error>) -> (RequestOutput, RetiredResources) {
match self {
Self::Send(r) => (
RequestOutput::Send(result),
RetiredResources::Send {
_fd: r.fd,
_write: r.write,
},
),
Self::Recv(r) => {
let result = match result {
Ok(()) => Ok((r.buf, r.offset)),
Err(err) => Err((r.buf, err)),
};
(
RequestOutput::Recv(result),
RetiredResources::Socket { _fd: r.fd },
)
}
Self::ReadAt(mut r) => {
let result = match result {
Ok(()) => Ok(r.buf),
Err(err) => {
r.buf.clear();
Err((r.buf, err))
}
};
(
RequestOutput::ReadAt(result),
RetiredResources::File {
_file: r.file,
_write: None,
},
)
}
Self::WriteAt(r) => {
let result = r.file.wrote(
r.state != WriteAtState::Writing,
matches!(r.state, WriteAtState::WritingSync | WriteAtState::Syncing),
result,
);
let result = match result {
Ok(()) if r.state == WriteAtState::Syncing => r.file.tracker.end_sync(r.seen),
result => result,
};
(
RequestOutput::WriteAt(result),
RetiredResources::File {
_file: r.file,
_write: Some(r.write),
},
)
}
Self::Sync(r) => {
let result = match result {
Ok(()) => r.file.tracker.end_sync(r.seen),
Err(error) => {
r.file.sync_failed(&error);
Err(error)
}
};
(
RequestOutput::Sync(result),
RetiredResources::File {
_file: r.file,
_write: None,
},
)
}
Self::Connect(r) => (
RequestOutput::Connect(result),
RetiredResources::Connect {
_fd: r.fd,
_address: r.address,
},
),
Self::Poll(r) => (
RequestOutput::Poll(result),
RetiredResources::Listener { _listener: r.fd },
),
}
}
}
#[derive(Debug)]
pub enum RequestOutput {
Send(Result<(), Error>),
Recv(Result<(IoBufMut, usize), (IoBufMut, Error)>),
ReadAt(Result<IoBufMut, (IoBufMut, Error)>),
WriteAt(Result<(), Error>),
Sync(Result<(), Error>),
Connect(Result<(), Error>),
Poll(Result<(), Error>),
}
pub enum RetiredResources {
Listener {
_listener: Arc<TcpListener>,
},
Socket {
_fd: Arc<OwnedFd>,
},
Send {
_fd: Arc<OwnedFd>,
_write: WriteBuffers,
},
File {
_file: Arc<Shared>,
_write: Option<WriteBuffers>,
},
Connect {
_fd: Arc<OwnedFd>,
_address: Box<SockAddr>,
},
}
enum CqeResult {
Retry,
Cancelled,
Error(i32),
Zero,
Positive(usize),
}
impl CqeResult {
const fn from_raw(result: i32, state: WaiterState) -> Self {
if result == -libc::EAGAIN || result == -libc::EWOULDBLOCK || result == -libc::EINTR {
Self::Retry
} else if result == -libc::ECANCELED && matches!(state, WaiterState::CancelRequested) {
Self::Cancelled
} else if result < 0 {
Self::Error(result)
} else if result == 0 {
Self::Zero
} else {
Self::Positive(result as usize)
}
}
}
fn scalar_len(remaining: usize) -> u32 {
remaining.min(u32::MAX as usize) as u32
}
pub struct SendRequest {
pub fd: Arc<OwnedFd>,
pub write: WriteBuffers,
pub deadline: Option<Instant>,
}
impl SendRequest {
fn build_sqe(&mut self) -> SqueueEntry {
let fd = Fd(self.fd.as_raw_fd());
match &mut self.write {
WriteBuffers::Single { buf, offset } => {
let bytes = &buf.as_ref()[*offset..];
let ptr = bytes.as_ptr();
let remaining = bytes.len();
opcode::Send::new(fd, ptr, scalar_len(remaining)).build()
}
WriteBuffers::Vectored {
bufs,
chunk,
offset,
iovecs,
..
} => {
let iovecs_len = fill_iovecs(bufs, *chunk, *offset, iovecs);
opcode::Writev::new(fd, iovecs.as_ptr(), iovecs_len).build()
}
}
}
fn on_cqe(&mut self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
match CqeResult::from_raw(result, state) {
CqeResult::Retry => None,
CqeResult::Cancelled => Some(Err(Error::Timeout)),
CqeResult::Error(_) | CqeResult::Zero => Some(Err(Error::SendFailed)),
CqeResult::Positive(n) => {
self.write.advance(n);
if self.write.is_complete() {
Some(Ok(()))
} else {
None
}
}
}
}
}
pub struct RecvRequest {
pub fd: Arc<OwnedFd>,
pub buf: IoBufMut,
pub offset: usize,
pub len: usize,
pub exact: bool,
pub deadline: Option<Instant>,
}
impl RecvRequest {
fn build_sqe(&mut self) -> SqueueEntry {
let fd = Fd(self.fd.as_raw_fd());
assert!(
self.offset <= self.len && self.len <= self.buf.capacity(),
"recv invariant violated: need offset <= len <= capacity"
);
let ptr = unsafe { self.buf.as_mut_ptr().add(self.offset) };
let remaining = self.len - self.offset;
opcode::Recv::new(fd, ptr, scalar_len(remaining)).build()
}
fn on_cqe(&mut self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
match CqeResult::from_raw(result, state) {
CqeResult::Retry => None,
CqeResult::Cancelled => Some(Err(Error::Timeout)),
CqeResult::Error(_) | CqeResult::Zero => Some(Err(Error::RecvFailed)),
CqeResult::Positive(n) => {
let remaining = self.len - self.offset;
assert!(
n <= remaining,
"recv CQE exceeds requested length: n={n} remaining={remaining}"
);
self.offset += n;
if !self.exact || self.offset >= self.len {
Some(Ok(()))
} else {
None
}
}
}
}
}
pub struct ReadAtRequest {
pub file: Arc<Shared>,
pub offset: u64,
pub read: usize,
pub buf: IoBufMut,
pub cache: Cache,
direct: Option<Direct>,
}
#[derive(Clone, Copy)]
struct Direct {
fd: RawFd,
skip: usize,
len: usize,
}
impl ReadAtRequest {
pub const fn new(file: Arc<Shared>, offset: u64, buf: IoBufMut, cache: Cache) -> Self {
Self {
file,
offset,
read: 0,
buf,
cache,
direct: None,
}
}
pub fn direct(file: Arc<Shared>, fd: RawFd, offset: u64, len: usize) -> Option<Self> {
if len == 0 {
return None;
}
let skip = (offset % DIRECT_ALIGN as u64) as usize;
let aligned_len = skip
.checked_add(len)?
.checked_next_multiple_of(DIRECT_ALIGN)?;
let aligned_offset = offset - skip as u64;
if aligned_offset.checked_add(aligned_len as u64)? > i64::MAX as u64 {
return None;
}
let mut buf = IoBufMut::try_with_alignment(aligned_len, NZUsize!(DIRECT_ALIGN))?;
unsafe { buf.set_len(aligned_len) };
Some(Self {
file,
offset: aligned_offset,
read: 0,
buf,
cache: Cache::Disabled,
direct: Some(Direct { fd, skip, len }),
})
}
fn build_sqe(&mut self) -> SqueueEntry {
let fd = Fd(self.file.as_raw_fd());
let len = self.buf.len();
assert!(
self.read <= len,
"read_at progress exceeds destination length"
);
let ptr = unsafe { self.buf.as_mut_ptr().add(self.read) };
let remaining = len - self.read;
let offset = self.offset + self.read as u64;
if let Some(direct) = self.direct {
let len = remaining.min(DIRECT_CHUNK);
assert!(
len > 0
&& len.is_multiple_of(DIRECT_ALIGN)
&& ptr.addr().is_multiple_of(DIRECT_ALIGN)
&& offset.is_multiple_of(DIRECT_ALIGN as u64),
"a direct read must be nonempty and aligned"
);
return opcode::Read::new(Fd(direct.fd), ptr, scalar_len(len))
.offset(offset)
.build();
}
let rw_flags = self.cache.rw_flag(&self.file.dont_cache_supported);
opcode::Read::new(fd, ptr, scalar_len(remaining))
.offset(offset)
.rw_flags(rw_flags)
.build()
}
fn on_cqe(&mut self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
if let Some(direct) = self.direct {
let end = direct.skip + direct.len;
match CqeResult::from_raw(result, state) {
CqeResult::Retry => return None,
CqeResult::Cancelled => return Some(Err(Error::ReadFailed)),
CqeResult::Error(code) if code == -libc::ECANCELED => {
return Some(Err(Error::ReadFailed));
}
CqeResult::Positive(n) => {
let submitted = (self.buf.len() - self.read).min(DIRECT_CHUNK);
assert!(
n <= submitted,
"a direct CQE must not exceed its submission: n={n} submitted={submitted}"
);
self.read += n;
if self.read >= end {
self.buf.truncate(end);
self.buf.advance(direct.skip);
return Some(Ok(()));
}
if n.is_multiple_of(DIRECT_ALIGN) {
return None;
}
}
CqeResult::Zero | CqeResult::Error(_) => {}
}
self.buf.truncate(end);
self.buf.advance(direct.skip);
self.read = 0;
self.offset += direct.skip as u64;
self.direct = None;
return None;
}
match CqeResult::from_raw(result, state) {
CqeResult::Retry => None,
CqeResult::Error(code)
if self.cache.fallback(&self.file.dont_cache_supported, code) =>
{
None
}
CqeResult::Cancelled | CqeResult::Error(_) => Some(Err(Error::ReadFailed)),
CqeResult::Zero => Some(Err(Error::BlobInsufficientLength)),
CqeResult::Positive(n) => {
let remaining = self.buf.len() - self.read;
assert!(
n <= remaining,
"read CQE exceeds requested length: n={n} remaining={remaining}"
);
self.read += n;
if self.read >= self.buf.len() {
Some(Ok(()))
} else {
None
}
}
}
}
}
pub enum Cache {
Enabled,
Disabled,
}
impl Cache {
fn rw_flag(&mut self, supported: &AtomicBool) -> i32 {
match self {
Self::Disabled if supported.load(Ordering::Relaxed) => libc::RWF_DONTCACHE,
Self::Disabled => {
*self = Self::Enabled;
0
}
Self::Enabled => 0,
}
}
fn fallback(&mut self, supported: &AtomicBool, code: i32) -> bool {
if code != -libc::EOPNOTSUPP {
return false;
}
match std::mem::replace(self, Self::Enabled) {
Self::Disabled => {
supported.store(false, Ordering::Relaxed);
true
}
Self::Enabled => false,
}
}
}
#[derive(Eq, PartialEq)]
pub enum WriteAtState {
Writing,
WritingSync,
WritingBeforeSync,
Syncing,
}
fn build_datasync_sqe(file: &File) -> SqueueEntry {
opcode::Fsync::new(Fd(file.as_raw_fd()))
.flags(io_uring::types::FsyncFlags::DATASYNC)
.build()
}
fn on_sync_cqe(state: WaiterState, result: i32) -> Option<Result<(), Error>> {
match CqeResult::from_raw(result, state) {
CqeResult::Retry => None,
CqeResult::Cancelled => {
let err = std::io::Error::from_raw_os_error(libc::ECANCELED);
Some(Err(Error::Io(err.into())))
}
CqeResult::Error(code) => {
let err = std::io::Error::from_raw_os_error(-code);
Some(Err(Error::Io(err.into())))
}
CqeResult::Zero | CqeResult::Positive(_) => Some(Ok(())),
}
}
pub struct WriteAtRequest {
pub file: Arc<Shared>,
pub offset: u64,
pub write: WriteBuffers,
pub state: WriteAtState,
pub cache: Cache,
seen: u64,
}
impl WriteAtRequest {
pub fn new(
file: Arc<Shared>,
offset: u64,
write: WriteBuffers,
state: WriteAtState,
cache: Cache,
) -> Self {
let seen = file.tracker.begin_sync();
Self {
file,
offset,
write,
state,
cache,
seen,
}
}
fn rw_flags(&mut self) -> i32 {
let sync = if self.state == WriteAtState::WritingSync {
libc::RWF_DSYNC
} else {
0
};
sync | self.cache.rw_flag(&self.file.dont_cache_supported)
}
fn build_sqe(&mut self) -> SqueueEntry {
if self.state == WriteAtState::Syncing {
return build_datasync_sqe(&self.file);
}
let fd = Fd(self.file.as_raw_fd());
let rw_flags = self.rw_flags();
match &mut self.write {
WriteBuffers::Single { buf, offset } => {
let bytes = &buf.as_ref()[*offset..];
let ptr = bytes.as_ptr();
opcode::Write::new(fd, ptr, scalar_len(bytes.len()))
.offset(self.offset)
.rw_flags(rw_flags)
.build()
}
WriteBuffers::Vectored {
bufs,
chunk,
offset,
iovecs,
..
} => {
let iovecs_len = fill_iovecs(bufs, *chunk, *offset, iovecs);
opcode::Writev::new(fd, iovecs.as_ptr(), iovecs_len)
.offset(self.offset)
.rw_flags(rw_flags)
.build()
}
}
}
fn on_cqe(&mut self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
if self.state == WriteAtState::Syncing {
return on_sync_cqe(state, result);
}
match CqeResult::from_raw(result, state) {
CqeResult::Retry => None,
CqeResult::Error(code)
if self.cache.fallback(&self.file.dont_cache_supported, code) =>
{
None
}
CqeResult::Error(code) if self.state == WriteAtState::WritingSync => Some(Err(
Error::Io(std::io::Error::from_raw_os_error(-code).into()),
)),
CqeResult::Cancelled | CqeResult::Error(_) | CqeResult::Zero => {
Some(Err(Error::WriteFailed))
}
CqeResult::Positive(n) => {
self.write.advance(n);
self.offset += n as u64;
if self.write.is_complete() {
if self.state == WriteAtState::WritingBeforeSync {
self.state = WriteAtState::Syncing;
None
} else {
Some(Ok(()))
}
} else {
None
}
}
}
}
}
pub struct SyncRequest {
pub file: Arc<Shared>,
seen: u64,
}
impl SyncRequest {
pub fn new(file: Arc<Shared>) -> Self {
let seen = file.tracker.begin_sync();
Self { file, seen }
}
fn build_sqe(&self) -> SqueueEntry {
build_datasync_sqe(&self.file)
}
fn on_cqe(&self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
on_sync_cqe(state, result)
}
}
pub struct ConnectRequest {
pub fd: Arc<OwnedFd>,
pub address: Box<SockAddr>,
pub deadline: Option<Instant>,
}
impl ConnectRequest {
fn build_sqe(&self) -> SqueueEntry {
let (address, len) = self.address.as_raw();
opcode::Connect::new(Fd(self.fd.as_raw_fd()), address, len).build()
}
fn on_cqe(&self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
if result == -libc::EISCONN || result == 0 {
return Some(Ok(()));
}
let result = if result == -libc::EALREADY {
-libc::EAGAIN
} else {
result
};
match CqeResult::from_raw(result, state) {
CqeResult::Retry => None,
CqeResult::Cancelled => Some(Err(Error::Timeout)),
CqeResult::Error(code) => Some(Err(Error::Io(
std::io::Error::from_raw_os_error(-code).into(),
))),
CqeResult::Zero | CqeResult::Positive(_) => Some(Err(Error::ConnectionFailed)),
}
}
}
pub struct PollRequest {
pub fd: Arc<TcpListener>,
pub deadline: Option<Instant>,
}
impl PollRequest {
fn build_sqe(&self) -> SqueueEntry {
opcode::PollAdd::new(Fd(self.fd.as_raw_fd()), libc::POLLIN as u32).build()
}
fn on_cqe(&self, state: WaiterState, result: i32) -> Option<Result<(), Error>> {
match CqeResult::from_raw(result, state) {
CqeResult::Retry => None,
CqeResult::Cancelled => Some(Err(Error::Timeout)),
CqeResult::Error(code) => Some(Err(Error::Io(
std::io::Error::from_raw_os_error(-code).into(),
))),
CqeResult::Zero | CqeResult::Positive(_) => Some(Ok(())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{iouring::waiter::tests::waiter_id, storage::hold::Hold};
use bytes::Bytes;
use std::{
net::SocketAddr,
os::unix::net::UnixStream,
panic::{AssertUnwindSafe, catch_unwind},
sync::{OnceLock, atomic::AtomicUsize},
};
const ACTIVE: WaiterState = WaiterState::Active { target_tick: None };
fn make_socket_fd() -> Arc<OwnedFd> {
let (left, _right) = UnixStream::pair().expect("failed to create unix socket pair");
Arc::new(left.into())
}
fn make_file() -> Arc<Shared> {
let (left, _right) = UnixStream::pair().expect("failed to create unix socket pair");
let file = File::from(OwnedFd::from(left));
static HOLD: OnceLock<Arc<Hold>> = OnceLock::new();
let hold = HOLD.get_or_init(|| {
Hold::acquire(
&std::env::temp_dir()
.join(format!("commonware_request_test_{}", std::process::id())),
)
.unwrap()
});
Shared::detached(file, hold.clone())
}
fn make_send_request() -> SendRequest {
SendRequest {
fd: make_socket_fd(),
write: IoBufs::from(IoBuf::from(b"hello")).into(),
deadline: None,
}
}
fn make_recv_request(exact: bool) -> RecvRequest {
RecvRequest {
fd: make_socket_fd(),
buf: IoBufMut::with_capacity(5),
offset: 0,
len: 5,
exact,
deadline: None,
}
}
fn make_read_request(file: Arc<Shared>, cache: Cache) -> ReadAtRequest {
ReadAtRequest::new(file, 0, IoBufMut::zeroed(5), cache)
}
fn make_write_request(file: Arc<Shared>, cache: Cache) -> WriteAtRequest {
WriteAtRequest::new(
file,
0,
IoBufs::from(IoBuf::from(b"hello")).into(),
WriteAtState::Writing,
cache,
)
}
fn make_connect_request(address: &str) -> ConnectRequest {
ConnectRequest {
fd: make_socket_fd(),
address: Box::new(address.parse::<SocketAddr>().unwrap().into()),
deadline: None,
}
}
fn make_poll_request() -> PollRequest {
PollRequest {
fd: Arc::new(TcpListener::bind("127.0.0.1:0").unwrap()),
deadline: None,
}
}
fn complete(mut request: Request, state: WaiterState, result: i32) -> RequestOutput {
let status = request.on_cqe(state, result).expect("terminal completion");
let (output, retired) = request.complete(status);
drop(retired);
output
}
fn make_direct_file() -> (Arc<Shared>, RawFd) {
let file = make_file();
let (direct, _peer) = UnixStream::pair().expect("failed to create unix socket pair");
let fd = file.set_direct(File::from(OwnedFd::from(direct)));
(file, fd)
}
fn make_direct_request(file: Arc<Shared>, fd: RawFd) -> ReadAtRequest {
ReadAtRequest::direct(file, fd, 4196, 4096).unwrap()
}
fn read_fields(sqe: &SqueueEntry) -> (u8, i32, u64, u64, u32, i32) {
const _: () = assert!(size_of::<SqueueEntry>() == 64);
let bytes: [u8; 64] = unsafe { std::mem::transmute_copy(sqe) };
let u32_at = |at: usize| u32::from_ne_bytes(bytes[at..at + 4].try_into().unwrap());
let u64_at = |at: usize| u64::from_ne_bytes(bytes[at..at + 8].try_into().unwrap());
(
bytes[0],
u32_at(4) as i32,
u64_at(8),
u64_at(16),
u32_at(24),
u32_at(28) as i32,
)
}
#[test]
fn test_write_cursor_retains_owners_across_batches() {
struct Owner {
dropped: Arc<AtomicUsize>,
bytes: [u8; 3],
}
impl AsRef<[u8]> for Owner {
fn as_ref(&self) -> &[u8] {
&self.bytes
}
}
impl Drop for Owner {
fn drop(&mut self) {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
let dropped = Arc::new(AtomicUsize::new(0));
let count = IOVEC_BATCH_SIZE + 2;
let mut bufs = IoBufs::default();
for _ in 0..count {
bufs.append(IoBuf::from(Bytes::from_owner(Owner {
dropped: dropped.clone(),
bytes: *b"abc",
})));
}
let mut write = WriteBuffers::from(bufs);
let inspect = |write: &mut WriteBuffers, expected: &[u8], expected_count| {
let WriteBuffers::Vectored {
bufs,
chunk,
offset,
iovecs,
..
} = write
else {
panic!("expected vectored buffers");
};
assert_eq!(fill_iovecs(bufs, *chunk, *offset, iovecs), expected_count);
assert_eq!(iovecs[0].iov_len, expected.len());
let first = unsafe {
std::slice::from_raw_parts(iovecs[0].iov_base.cast::<u8>(), iovecs[0].iov_len)
};
assert_eq!(first, expected);
};
inspect(&mut write, b"abc", IOVEC_BATCH_SIZE as u32);
write.advance(1);
inspect(&mut write, b"bc", IOVEC_BATCH_SIZE as u32);
write.advance(2);
inspect(&mut write, b"abc", IOVEC_BATCH_SIZE as u32);
write.advance(3 * IOVEC_BATCH_SIZE);
inspect(&mut write, b"abc", 1);
write.advance(3);
assert!(write.is_complete());
assert_eq!(dropped.load(Ordering::Relaxed), 0);
drop(write);
assert_eq!(dropped.load(Ordering::Relaxed), count);
}
#[test]
fn test_write_cursor_single_and_overrun() {
let mut write = WriteBuffers::from(IoBufs::from(IoBuf::from(b"abc")));
write.advance(1);
let WriteBuffers::Single { buf, offset } = &write else {
panic!("expected single buffer");
};
assert_eq!(buf.as_ref(), b"abc");
assert_eq!(*offset, 1);
assert!(catch_unwind(AssertUnwindSafe(|| write.advance(3))).is_err());
assert_eq!(write.remaining_len(), 2);
write.advance(2);
assert!(write.is_complete());
}
#[test]
fn test_connect_completion_rules() {
for address in ["127.0.0.1:1234", "[::1]:1234"] {
let connect = make_connect_request(address);
let pointer = connect.address.as_raw();
connect.build_sqe();
let connect = std::hint::black_box(connect);
assert_eq!(connect.address.as_raw(), pointer);
for result in [-libc::EALREADY, -libc::EAGAIN, -libc::EINTR] {
assert!(connect.on_cqe(ACTIVE, result).is_none());
}
}
for result in [0, -libc::EISCONN] {
let request = Request::Connect(make_connect_request("127.0.0.1:1234"));
assert!(matches!(
complete(request, WaiterState::CancelRequested, result),
RequestOutput::Connect(Ok(()))
));
}
let request = Request::Connect(make_connect_request("127.0.0.1:1234"));
assert!(matches!(
complete(request, WaiterState::CancelRequested, -libc::ECANCELED),
RequestOutput::Connect(Err(Error::Timeout))
));
for code in [libc::ECONNREFUSED, libc::ECANCELED] {
let request = Request::Connect(make_connect_request("127.0.0.1:1234"));
let RequestOutput::Connect(Err(Error::Io(error))) = complete(request, ACTIVE, -code)
else {
panic!("expected connect I/O error");
};
assert_eq!(error.raw_os_error(), Some(code));
}
}
#[test]
fn test_poll_completion_rules() {
let poll = make_poll_request();
assert!(poll.on_cqe(ACTIVE, -libc::EINTR).is_none());
for result in [0, libc::POLLIN as i32] {
let request = Request::Poll(make_poll_request());
assert!(matches!(
complete(request, WaiterState::CancelRequested, result),
RequestOutput::Poll(Ok(()))
));
}
let request = Request::Poll(make_poll_request());
assert!(matches!(
complete(request, WaiterState::CancelRequested, -libc::ECANCELED),
RequestOutput::Poll(Err(Error::Timeout))
));
for code in [libc::ECANCELED, libc::EBADF] {
let request = Request::Poll(make_poll_request());
let RequestOutput::Poll(Err(Error::Io(error))) = complete(request, ACTIVE, -code)
else {
panic!("expected poll I/O error");
};
assert_eq!(error.raw_os_error(), Some(code));
}
}
#[test]
fn test_partial_progress_retires_reentrant_panicking_owner() {
struct Owner {
local: Arc<commonware_utils::sync::Mutex<usize>>,
bytes: [u8; 3],
panic: bool,
}
impl AsRef<[u8]> for Owner {
fn as_ref(&self) -> &[u8] {
&self.bytes
}
}
impl Drop for Owner {
fn drop(&mut self) {
*self
.local
.try_lock()
.expect("owner dropped under local borrow") += 1;
if self.panic {
panic!("external owner panic");
}
}
}
let local = Arc::new(commonware_utils::sync::Mutex::new(0));
let mut bufs = IoBufs::from(IoBuf::from(Bytes::from_owner(Owner {
local: local.clone(),
bytes: *b"abc",
panic: true,
})));
bufs.append(IoBuf::from(Bytes::from_owner(Owner {
local: local.clone(),
bytes: *b"def",
panic: false,
})));
let mut request = Request::Send(SendRequest {
fd: make_socket_fd(),
write: bufs.into(),
deadline: None,
});
let guard = local.lock();
assert!(request.on_cqe(ACTIVE, 3).is_none());
let (output, retired) = request.complete(Err(Error::Timeout));
assert!(matches!(output, RequestOutput::Send(Err(Error::Timeout))));
assert_eq!(*guard, 0);
drop(guard);
let panic = catch_unwind(AssertUnwindSafe(|| drop(retired)))
.expect_err("owner panic was not caught");
assert_eq!(panic.downcast_ref::<&str>(), Some(&"external owner panic"));
assert_eq!(*local.lock(), 2);
}
#[test]
fn test_cqe_result_from_raw_retryable_codes() {
for code in [-libc::EAGAIN, -libc::EWOULDBLOCK, -libc::EINTR] {
assert!(matches!(
CqeResult::from_raw(code, ACTIVE),
CqeResult::Retry
));
}
for code in [0, -libc::EINVAL, -libc::ETIMEDOUT] {
assert!(!matches!(
CqeResult::from_raw(code, ACTIVE),
CqeResult::Retry
));
}
}
#[test]
fn test_request_metadata_and_sqe_tags() {
let id = waiter_id(3, 7);
for deadline in [None, Some(Instant::now())] {
let mut send = make_send_request();
send.deadline = deadline;
let mut recv = make_recv_request(true);
recv.deadline = deadline;
let mut connect = make_connect_request("127.0.0.1:1234");
connect.deadline = deadline;
let mut poll = make_poll_request();
poll.deadline = deadline;
let requests = [
(Request::Send(send), opcode::Send::CODE, deadline, false),
(Request::Recv(recv), opcode::Recv::CODE, deadline, false),
(
Request::ReadAt(make_read_request(make_file(), Cache::Enabled)),
opcode::Read::CODE,
None,
false,
),
(
Request::WriteAt(make_write_request(make_file(), Cache::Enabled)),
opcode::Write::CODE,
None,
true,
),
(
Request::Sync(SyncRequest::new(make_file())),
opcode::Fsync::CODE,
None,
true,
),
(
Request::Connect(connect),
opcode::Connect::CODE,
deadline,
false,
),
(Request::Poll(poll), opcode::PollAdd::CODE, deadline, false),
];
for (mut request, opcode, deadline, retained) in requests {
assert_eq!(request.deadline(), deadline);
assert_eq!(request.retains_on_orphan(), retained);
let sqe = request.build_sqe(id);
assert_eq!(sqe.get_opcode(), opcode as u32);
assert_eq!(sqe.get_user_data(), id.user_data());
}
}
}
#[test]
fn test_read_builders_reject_invalid_buffer_bounds() {
for (progress, len) in [(6, 5), (0, 6)] {
let mut recv = make_recv_request(true);
recv.offset = progress;
recv.len = len;
assert!(catch_unwind(AssertUnwindSafe(|| recv.build_sqe())).is_err());
}
let mut read = make_read_request(make_file(), Cache::Enabled);
read.read = 6;
assert!(catch_unwind(AssertUnwindSafe(|| read.build_sqe())).is_err());
}
#[test]
fn test_scalar_length_prefix_boundary() {
for len in [0, 1, u32::MAX as usize - 1, u32::MAX as usize] {
assert_eq!(scalar_len(len), len as u32);
}
for len in [u32::MAX as usize + 1, usize::MAX] {
assert_eq!(scalar_len(len), u32::MAX);
}
}
#[test]
fn test_scalar_builders_preserve_partial_progress() {
let deadline = Some(Instant::now());
let mut send = SendRequest {
fd: make_socket_fd(),
write: IoBufs::from(IoBuf::from(b"hello")).into(),
deadline,
};
let mut recv = RecvRequest {
fd: make_socket_fd(),
buf: IoBufMut::with_capacity(5),
offset: 0,
len: 5,
exact: true,
deadline,
};
let mut read = make_read_request(make_file(), Cache::Enabled);
read.offset = 7;
for progress in [2, 3] {
assert_eq!(send.build_sqe().get_opcode(), opcode::Send::CODE as u32);
assert_eq!(recv.build_sqe().get_opcode(), opcode::Recv::CODE as u32);
assert_eq!(read.build_sqe().get_opcode(), opcode::Read::CODE as u32);
assert_eq!(send.on_cqe(ACTIVE, progress).is_some(), progress == 3);
assert_eq!(recv.on_cqe(ACTIVE, progress).is_some(), progress == 3);
assert_eq!(read.on_cqe(ACTIVE, progress).is_some(), progress == 3);
assert_eq!(send.deadline, deadline);
assert_eq!(recv.deadline, deadline);
}
assert!(send.write.is_complete());
assert_eq!(recv.offset, 5);
assert_eq!(read.read, 5);
assert_eq!(read.offset, 7);
}
#[test]
fn test_active_send_paths() {
let mut request = Request::Send(make_send_request());
assert!(request.on_cqe(ACTIVE, -libc::EAGAIN).is_none());
let mut send = make_send_request();
let mut bufs = IoBufs::from(IoBuf::from(b"abc"));
bufs.append(IoBuf::from(b"de"));
send.write = bufs.into();
let mut request = Request::Send(send);
assert!(request.on_cqe(ACTIVE, 3).is_none());
assert!(matches!(
complete(request, ACTIVE, 2),
RequestOutput::Send(Ok(()))
));
for result in [0, -libc::EIO] {
let request = Request::Send(make_send_request());
assert!(matches!(
complete(request, ACTIVE, result),
RequestOutput::Send(Err(Error::SendFailed))
));
}
let request = Request::Send(make_send_request());
assert!(matches!(
complete(request, WaiterState::CancelRequested, -libc::ECANCELED),
RequestOutput::Send(Err(Error::Timeout))
));
let request = Request::Send(make_send_request());
assert!(matches!(
complete(request, WaiterState::CancelRequested, 5),
RequestOutput::Send(Ok(()))
));
}
#[test]
fn test_active_recv_paths() {
let mut request = Request::Recv(make_recv_request(true));
assert!(request.on_cqe(ACTIVE, -libc::EAGAIN).is_none());
let request = Request::Recv(make_recv_request(false));
assert!(matches!(
complete(request, ACTIVE, 3),
RequestOutput::Recv(Ok((_, 3)))
));
for result in [0, -libc::EIO] {
let request = Request::Recv(make_recv_request(true));
assert!(matches!(
complete(request, ACTIVE, result),
RequestOutput::Recv(Err((_, Error::RecvFailed)))
));
}
let request = Request::Recv(make_recv_request(true));
assert!(matches!(
complete(request, WaiterState::CancelRequested, -libc::ECANCELED),
RequestOutput::Recv(Err((_, Error::Timeout)))
));
let request = Request::Recv(make_recv_request(true));
assert!(matches!(
complete(request, WaiterState::CancelRequested, 5),
RequestOutput::Recv(Ok((_, 5)))
));
let mut request = Request::Recv(make_recv_request(true));
let overflow = catch_unwind(AssertUnwindSafe(|| request.on_cqe(ACTIVE, 6)));
assert!(overflow.is_err());
}
#[test]
fn test_active_read_at_paths() {
let mut request = Request::ReadAt(make_read_request(make_file(), Cache::Enabled));
assert!(request.on_cqe(ACTIVE, -libc::EAGAIN).is_none());
assert!(request.on_cqe(ACTIVE, 2).is_none());
assert!(matches!(
complete(request, ACTIVE, 3),
RequestOutput::ReadAt(Ok(_))
));
let request = Request::ReadAt(make_read_request(make_file(), Cache::Enabled));
assert!(matches!(
complete(request, ACTIVE, 0),
RequestOutput::ReadAt(Err((_, Error::BlobInsufficientLength)))
));
let request = Request::ReadAt(make_read_request(make_file(), Cache::Enabled));
assert!(matches!(
complete(request, ACTIVE, -libc::EIO),
RequestOutput::ReadAt(Err((_, Error::ReadFailed)))
));
let request = Request::ReadAt(make_read_request(make_file(), Cache::Enabled));
assert!(matches!(
complete(request, WaiterState::CancelRequested, -libc::ECANCELED),
RequestOutput::ReadAt(Err((_, Error::ReadFailed)))
));
}
#[test]
fn test_uncached_read_fallback_preserves_progress_and_is_shared_with_writes() {
let file = make_file();
let mut read = make_read_request(file.clone(), Cache::Disabled);
assert_eq!(
read.cache.rw_flag(&file.dont_cache_supported),
libc::RWF_DONTCACHE
);
assert!(read.on_cqe(ACTIVE, 2).is_none());
assert_eq!(read.read, 2);
assert_eq!(
read.cache.rw_flag(&file.dont_cache_supported),
libc::RWF_DONTCACHE
);
assert!(read.on_cqe(ACTIVE, -libc::EOPNOTSUPP).is_none());
assert_eq!(read.read, 2);
assert!(!file.dont_cache_supported.load(Ordering::Relaxed));
assert_eq!(read.cache.rw_flag(&file.dont_cache_supported), 0);
let mut sibling_write = make_write_request(file, Cache::Disabled);
assert_eq!(sibling_write.rw_flags(), 0);
let file = make_file();
let mut write = make_write_request(file.clone(), Cache::Disabled);
assert_eq!(write.rw_flags(), libc::RWF_DONTCACHE);
assert!(write.on_cqe(ACTIVE, -libc::EOPNOTSUPP).is_none());
let mut sibling_read = make_read_request(file.clone(), Cache::Disabled);
assert_eq!(sibling_read.cache.rw_flag(&file.dont_cache_supported), 0);
let file = make_file();
let mut failing_read = make_read_request(file.clone(), Cache::Disabled);
assert_eq!(
failing_read.cache.rw_flag(&file.dont_cache_supported),
libc::RWF_DONTCACHE
);
let result = failing_read.on_cqe(ACTIVE, -libc::EIO);
assert!(file.dont_cache_supported.load(Ordering::Relaxed));
assert!(matches!(result, Some(Err(Error::ReadFailed))));
}
#[test]
fn test_queued_cache_fallbacks_retry() {
let file = make_file();
let mut first = make_read_request(file.clone(), Cache::Disabled);
let mut second = make_read_request(file.clone(), Cache::Disabled);
assert_eq!(
first.cache.rw_flag(&file.dont_cache_supported),
libc::RWF_DONTCACHE
);
assert_eq!(
second.cache.rw_flag(&file.dont_cache_supported),
libc::RWF_DONTCACHE
);
assert!(first.on_cqe(ACTIVE, -libc::EOPNOTSUPP).is_none());
assert!(second.on_cqe(ACTIVE, -libc::EOPNOTSUPP).is_none());
assert!(!file.dont_cache_supported.load(Ordering::Relaxed));
assert_eq!(first.cache.rw_flag(&file.dont_cache_supported), 0);
assert_eq!(second.cache.rw_flag(&file.dont_cache_supported), 0);
}
#[commonware_macros::test_group("miri")]
#[test]
fn test_direct_read_covers_the_aligned_superset() {
let (file, fd) = make_direct_file();
let top = (1u64 << 63) - 2 * DIRECT_ALIGN as u64;
for ((offset, len), (aligned_offset, skip, aligned_len)) in [
((4096, 4096), (4096, 0, 4096)),
((4103, 10), (4096, 7, 4096)),
((8189, 6), (4096, 4093, 8192)),
((8192, 1), (8192, 0, 4096)),
((8, 4096), (0, 8, 8192)),
((top + 10, 4086), (top, 10, 4096)),
] {
let mut read = ReadAtRequest::direct(file.clone(), fd, offset, len).unwrap();
assert_eq!((read.offset, read.read), (aligned_offset, 0));
assert_eq!(
(read.buf.len(), read.buf.capacity()),
(aligned_len, aligned_len)
);
let base = read.buf.as_mut_ptr() as u64;
assert!(base.is_multiple_of(DIRECT_ALIGN as u64));
assert!(matches!(read.cache, Cache::Disabled));
let direct = read.direct.unwrap();
assert_eq!((direct.fd, direct.skip, direct.len), (fd, skip, len));
assert_eq!(
read_fields(&read.build_sqe()),
(
opcode::Read::CODE,
fd,
aligned_offset,
base,
aligned_len as u32,
0
)
);
}
for (offset, len) in [
(0, 0),
(4095, usize::MAX - 4094),
(0, usize::MAX - 100),
(u64::MAX - 1, 1),
(top + DIRECT_ALIGN as u64, 1),
] {
assert!(
ReadAtRequest::direct(file.clone(), fd, offset, len).is_none(),
"{offset} {len}"
);
}
}
#[test]
fn test_direct_chunks_progress_under_every_page_cap() {
assert_eq!(DIRECT_CHUNK % DIRECT_ALIGN, 0);
assert!(DIRECT_CHUNK <= i32::MAX as usize);
let (file, fd) = make_direct_file();
for page in [4096usize, 16 << 10, 64 << 10] {
let kernel_cap = i32::MAX as usize & !(page - 1);
for skip in [0, 100] {
let len = DIRECT_CHUNK + 1 - skip;
let mut read = ReadAtRequest::direct(file.clone(), fd, skip as u64, len).unwrap();
assert_eq!(read.buf.len(), DIRECT_CHUNK + DIRECT_ALIGN);
let mut chunks = 0;
loop {
let (_, _, offset, _, submitted, _) = read_fields(&read.build_sqe());
let submitted = submitted as usize;
assert_eq!(offset, read.read as u64);
assert!(submitted > 0 && submitted <= DIRECT_CHUNK);
assert!(submitted.is_multiple_of(DIRECT_ALIGN));
chunks += 1;
let result = i32::try_from(submitted.min(kernel_cap)).unwrap();
match read.on_cqe(ACTIVE, result) {
None => assert!(read.read.is_multiple_of(DIRECT_ALIGN)),
Some(result) => {
assert!(result.is_ok());
break;
}
}
}
assert_eq!(chunks, 2);
assert_eq!(read.buf.len(), len);
}
}
}
#[commonware_macros::test_group("miri")]
#[test]
fn test_direct_read_completion_sequences() {
#[derive(Debug, PartialEq)]
enum Served {
Direct,
Buffered,
Failed,
TimedOut,
Pending,
}
const fn byte(offset: u64) -> u8 {
(offset % 251) as u8
}
fn kernel_read(read: &mut ReadAtRequest, n: usize) {
let (_, fd, offset, address, len, flags) = read_fields(&read.build_sqe());
let phase = match read.direct {
Some(direct) => (direct.fd, 0),
None => (read.file.as_raw_fd(), libc::RWF_DONTCACHE),
};
assert_eq!((fd, flags), phase);
let base = read.buf.as_mut_ptr();
assert_eq!(address, base as u64 + read.read as u64);
assert!(n <= len as usize);
for i in 0..n {
unsafe { base.add(read.read + i).write(byte(offset + i as u64)) };
}
}
fn serve(
offset: u64,
len: usize,
seed: Option<u8>,
completions: &[i32],
cancel_from: usize,
) -> (Served, Option<Vec<u8>>) {
let (file, fd) = make_direct_file();
let mut read = ReadAtRequest::direct(file.clone(), fd, offset, len).unwrap();
if let Some(seed) = seed {
unsafe { std::ptr::write_bytes(read.buf.as_mut_ptr(), seed, read.buf.len()) };
}
for (index, &result) in completions.iter().enumerate() {
if result > 0 {
kernel_read(&mut read, result as usize);
}
let cancelling = index >= cancel_from;
let state = if cancelling {
WaiterState::CancelRequested
} else {
ACTIVE
};
let outcome = read.on_cqe(state, result);
assert!(file.dont_cache_supported.load(Ordering::Relaxed));
match outcome {
Some(Ok(())) => {
let buf = read.buf.freeze().try_into_mut().unwrap();
assert_eq!(buf.len(), len);
return (Served::Direct, Some(buf.as_ref().to_vec()));
}
Some(Err(Error::ReadFailed)) => {
let (output, _) = Request::ReadAt(read).complete(Err(Error::ReadFailed));
let RequestOutput::ReadAt(Err((buf, _))) = output else {
panic!("a failed read must return its buffer");
};
assert_eq!(buf.len(), 0);
return (Served::Failed, None);
}
Some(Err(error)) => panic!("direct reads must fail with ReadFailed: {error:?}"),
None if cancelling => return (Served::TimedOut, None),
None if read.direct.is_none() => {
assert_eq!((read.offset, read.read, read.buf.len()), (offset, 0, len));
kernel_read(&mut read, len);
let result = i32::try_from(len).unwrap();
assert!(matches!(read.on_cqe(ACTIVE, result), Some(Ok(()))));
return (Served::Buffered, Some(read.buf.as_ref().to_vec()));
}
None => assert!(read.read.is_multiple_of(DIRECT_ALIGN)),
}
}
(Served::Pending, None)
}
const NEVER: usize = usize::MAX;
let page = DIRECT_ALIGN as i32;
let long = 3 * DIRECT_ALIGN - 200;
let cases: &[(u64, usize, &[i32], usize, Served)] = &[
(0, DIRECT_ALIGN, &[page], NEVER, Served::Direct),
(4196, 50, &[page], NEVER, Served::Direct),
(4093, 6, &[2 * page], NEVER, Served::Direct),
(8192 + 10, 100, &[200], NEVER, Served::Direct),
(8192 + 10, 100, &[110], NEVER, Served::Direct),
(8192 + 10, 100, &[109], NEVER, Served::Buffered),
(
100,
long,
&[-libc::EAGAIN, page, -libc::EINTR, 2 * page],
NEVER,
Served::Direct,
),
(100, long, &[page, page, page - 100], NEVER, Served::Direct),
(
100,
long,
&[page, page, page - 101],
NEVER,
Served::Buffered,
),
(100, long, &[page, 10], NEVER, Served::Buffered),
(100, long, &[page, 0], NEVER, Served::Buffered),
(
100,
long,
&[page, -libc::EOPNOTSUPP],
NEVER,
Served::Buffered,
),
(7, 10, &[-libc::EINVAL], NEVER, Served::Buffered),
(7, 10, &[-libc::EOPNOTSUPP], NEVER, Served::Buffered),
(7, 10, &[-libc::EFAULT], NEVER, Served::Buffered),
(7, 10, &[-libc::ENOMEM], NEVER, Served::Buffered),
(4096, 4096, &[-libc::EIO], NEVER, Served::Buffered),
(7, 10, &[-libc::ECANCELED], NEVER, Served::Failed),
(100, long, &[page, -libc::EAGAIN], NEVER, Served::Pending),
(4196, 50, &[page], 0, Served::Direct),
(8192 + 10, 100, &[110], 0, Served::Direct),
(100, long, &[page, 2 * page], 1, Served::Direct),
(100, long, &[page, page, page - 100], 2, Served::Direct),
(7, 10, &[-libc::EAGAIN], 0, Served::TimedOut),
(100, long, &[page], 0, Served::TimedOut),
(100, long, &[page, page - 1], 1, Served::TimedOut),
(100, long, &[page, page, page - 101], 2, Served::TimedOut),
(8192 + 10, 100, &[109], 0, Served::TimedOut),
(100, long, &[page, 0], 1, Served::TimedOut),
(100, long, &[page, -libc::EAGAIN], 1, Served::TimedOut),
(7, 10, &[-libc::EINVAL], 0, Served::TimedOut),
(7, 10, &[-libc::ECANCELED], 0, Served::Failed),
];
for seed in [None, Some(0xEE), Some(0x00)] {
for (offset, len, completions, cancel_from, outcome) in cases {
let (served, bytes) = serve(*offset, *len, seed, completions, *cancel_from);
if let Some(bytes) = bytes {
let expected: Vec<u8> = (*offset..*offset + *len as u64).map(byte).collect();
assert_eq!(bytes, expected, "{offset} {len} {completions:?} {seed:?}");
}
assert_eq!(
&served, outcome,
"{offset} {len} {completions:?} {cancel_from}"
);
}
}
let (file, fd) = make_direct_file();
for (prefix, result) in [(0, 8193), (4096, 4097)] {
let mut read = make_direct_request(file.clone(), fd);
if prefix > 0 {
assert!(read.on_cqe(ACTIVE, prefix).is_none());
}
let overflow = catch_unwind(AssertUnwindSafe(|| read.on_cqe(ACTIVE, result)));
assert!(overflow.is_err());
}
}
#[commonware_macros::test_group("miri")]
#[test]
fn test_direct_fallback_rereads_the_range_and_keeps_buffered_progress() {
let (file, fd) = make_direct_file();
let buffered_fd = file.as_raw_fd();
let mut read = make_direct_request(file.clone(), fd);
let base = read.buf.as_mut_ptr() as u64;
assert!(read.on_cqe(ACTIVE, 4096).is_none());
assert!(read.on_cqe(ACTIVE, -libc::EOPNOTSUPP).is_none());
assert_eq!(
read_fields(&read.build_sqe()),
(
opcode::Read::CODE,
buffered_fd,
4196,
base + 100,
4096,
libc::RWF_DONTCACHE
)
);
assert!(read.on_cqe(ACTIVE, 3).is_none());
assert_eq!(read_fields(&read.build_sqe()).2, 4199);
assert!(read.on_cqe(ACTIVE, -libc::EOPNOTSUPP).is_none());
assert!(!file.dont_cache_supported.load(Ordering::Relaxed));
assert_eq!(read.read, 3);
assert_eq!(
read_fields(&read.build_sqe()),
(opcode::Read::CODE, buffered_fd, 4199, base + 103, 4093, 0)
);
assert!(matches!(read.on_cqe(ACTIVE, 4093), Some(Ok(()))));
assert_eq!(read.buf.len(), 4096);
}
#[test]
fn test_large_single_write_preserves_progress_and_durability() {
let len = u32::MAX as usize + 1;
let buf = IoBuf::from(vec![0; len]);
for state in [
WriteAtState::Writing,
WriteAtState::WritingSync,
WriteAtState::WritingBeforeSync,
] {
let trailing_sync = state == WriteAtState::WritingBeforeSync;
let mut write = WriteAtRequest::new(
make_file(),
17,
IoBufs::from(buf.clone()).into(),
state,
Cache::Enabled,
);
for _ in 0..2 {
assert_eq!(write.build_sqe().get_opcode(), opcode::Write::CODE as u32);
assert!(write.on_cqe(ACTIVE, i32::MAX).is_none());
}
assert_eq!(write.write.remaining_len(), 2);
assert_eq!(write.build_sqe().get_opcode(), opcode::Write::CODE as u32);
let mut result = write.on_cqe(ACTIVE, 2);
assert_eq!(write.offset, 17 + len as u64);
assert!(write.write.is_complete());
if trailing_sync {
assert!(result.is_none());
assert_eq!(write.build_sqe().get_opcode(), opcode::Fsync::CODE as u32);
result = write.on_cqe(ACTIVE, 0);
}
assert!(matches!(result, Some(Ok(()))));
}
}
#[test]
fn test_active_write_at_paths() {
let mut write = make_write_request(make_file(), Cache::Enabled);
assert_eq!(write.rw_flags(), 0);
let mut request = Request::WriteAt(write);
assert!(request.on_cqe(ACTIVE, -libc::EAGAIN).is_none());
assert!(request.on_cqe(ACTIVE, 2).is_none());
assert!(matches!(
complete(request, ACTIVE, 3),
RequestOutput::WriteAt(Ok(()))
));
let mut write = make_write_request(make_file(), Cache::Enabled);
let mut bufs = IoBufs::from(IoBuf::from(b"abc"));
bufs.append(IoBuf::from(b"de"));
write.write = bufs.into();
let mut request = Request::WriteAt(write);
assert!(request.on_cqe(ACTIVE, 4).is_none());
assert!(matches!(
complete(request, ACTIVE, 1),
RequestOutput::WriteAt(Ok(()))
));
for result in [0, -libc::EIO, -libc::ECANCELED] {
let request = Request::WriteAt(make_write_request(make_file(), Cache::Enabled));
assert!(matches!(
complete(request, ACTIVE, result),
RequestOutput::WriteAt(Err(Error::WriteFailed))
));
}
let mut write = make_write_request(make_file(), Cache::Enabled);
write.state = WriteAtState::WritingBeforeSync;
assert!(matches!(
complete(Request::WriteAt(write), ACTIVE, -libc::EIO),
RequestOutput::WriteAt(Err(Error::WriteFailed))
));
let mut write = make_write_request(make_file(), Cache::Enabled);
write.state = WriteAtState::WritingSync;
assert_eq!(write.rw_flags(), libc::RWF_DSYNC);
let file = write.file.clone();
assert!(matches!(
complete(Request::WriteAt(write), ACTIVE, -libc::EINVAL),
RequestOutput::WriteAt(Err(Error::Io(error)))
if error.raw_os_error() == Some(libc::EINVAL)
));
assert!(matches!(
file.tracker.failure(),
Some(Error::BlobSyncFailed(_, _, error))
if error.raw_os_error() == Some(libc::EINVAL)
));
for (state, result) in [
(ACTIVE, 0),
(WaiterState::CancelRequested, -libc::ECANCELED),
] {
let mut write = make_write_request(make_file(), Cache::Enabled);
write.state = WriteAtState::WritingSync;
assert!(matches!(
complete(Request::WriteAt(write), state, result),
RequestOutput::WriteAt(Err(Error::WriteFailed))
));
}
}
#[test]
fn test_uncached_sync_write_retries_without_hint_when_unsupported() {
let mut request = WriteAtRequest::new(
make_file(),
0,
IoBufs::from(IoBuf::from(b"hello")).into(),
WriteAtState::WritingSync,
Cache::Disabled,
);
assert_eq!(request.rw_flags(), libc::RWF_DSYNC | libc::RWF_DONTCACHE);
assert!(request.on_cqe(ACTIVE, -libc::EOPNOTSUPP).is_none());
assert!(!request.file.dont_cache_supported.load(Ordering::Relaxed));
request.cache = Cache::Disabled;
assert_eq!(request.rw_flags(), libc::RWF_DSYNC);
assert!(
!request
.cache
.fallback(&request.file.dont_cache_supported, -libc::EOPNOTSUPP)
);
}
#[test]
fn test_active_sync_paths() {
let mut request = Request::Sync(SyncRequest::new(make_file()));
assert!(request.on_cqe(ACTIVE, -libc::EINTR).is_none());
for code in [libc::ECANCELED, libc::EIO] {
let request = Request::Sync(SyncRequest::new(make_file()));
let RequestOutput::Sync(Err(Error::Io(error))) = complete(request, ACTIVE, -code)
else {
panic!("expected sync I/O error");
};
assert_eq!(error.raw_os_error(), Some(code));
}
for result in [0, 1] {
let request = Request::Sync(SyncRequest::new(make_file()));
assert!(matches!(
complete(request, ACTIVE, result),
RequestOutput::Sync(Ok(()))
));
}
}
#[test]
fn test_sync_completion_credits_only_its_successful_barrier() {
for trailing in [false, true] {
for success in [false, true] {
for later_write in [false, true] {
let file = make_file();
file.tracker.write();
file.tracker.complete();
let mut request = if trailing {
Request::WriteAt(WriteAtRequest::new(
file.clone(),
0,
IoBufs::from(IoBuf::from(b"hello")).into(),
WriteAtState::WritingBeforeSync,
Cache::Enabled,
))
} else {
Request::Sync(SyncRequest::new(file.clone()))
};
if trailing {
assert!(request.on_cqe(ACTIVE, 5).is_none());
}
if later_write {
file.tracker.write();
file.tracker.complete();
}
let result = if success { Ok(()) } else { Err(Error::Timeout) };
let (_, retired) = request.complete(result);
drop(retired);
assert_eq!(file.tracker.is_dirty(), !success || later_write);
}
}
}
}
#[test]
fn test_durability_completion_retains_failure() {
for state in [
None,
Some(WriteAtState::WritingSync),
Some(WriteAtState::Syncing),
] {
for failure_first in [false, true] {
let file = make_file();
file.tracker.write();
file.tracker.complete();
let mut failed = Request::Sync(SyncRequest::new(file.clone()));
let mut successful = state.as_ref().map_or_else(
|| Request::Sync(SyncRequest::new(file.clone())),
|state| {
Request::WriteAt(WriteAtRequest::new(
file.clone(),
0,
IoBufs::from(IoBuf::from(b"hello")).into(),
if *state == WriteAtState::Syncing {
WriteAtState::WritingBeforeSync
} else {
WriteAtState::WritingSync
},
Cache::Enabled,
))
},
);
if state == Some(WriteAtState::Syncing) {
assert!(successful.on_cqe(ACTIVE, 5).is_none());
}
let finish_failure = || {
let result = failed.on_cqe(ACTIVE, -libc::EIO).unwrap();
let (output, retired) = failed.complete(result);
assert!(matches!(output, RequestOutput::Sync(Err(Error::Io(_)))));
assert!(matches!(
file.tracker.failure(),
Some(Error::BlobSyncFailed(_, _, error))
if error.raw_os_error() == Some(libc::EIO)
));
drop(retired);
};
let finish_success = || {
let result = successful
.on_cqe(
ACTIVE,
if state == Some(WriteAtState::WritingSync) {
5
} else {
0
},
)
.unwrap();
let (output, retired) = successful.complete(result);
let result = match output {
RequestOutput::Sync(result) | RequestOutput::WriteAt(result) => result,
_ => unreachable!(),
};
if failure_first {
assert!(
matches!(&result, Err(Error::BlobSyncFailed(_, _, error))
if error.raw_os_error() == Some(libc::EIO)),
"acknowledged after a retained failure: {result:?}"
);
assert!(file.tracker.is_dirty());
} else {
result.unwrap();
assert_eq!(
file.tracker.is_dirty(),
state == Some(WriteAtState::WritingSync)
);
}
drop(retired);
};
if failure_first {
finish_failure();
finish_success();
} else {
finish_success();
finish_failure();
}
}
}
}
#[test]
fn test_complete_preserves_status_and_read_buffers() {
let request = Request::Send(make_send_request());
let (output, retired) = request.complete(Err(Error::Timeout));
assert!(matches!(output, RequestOutput::Send(Err(Error::Timeout))));
drop(retired);
let mut recv = make_recv_request(true);
let pointer = recv.buf.as_mut_ptr();
let (output, retired) = Request::Recv(recv).complete(Err(Error::Timeout));
let RequestOutput::Recv(Err((mut buf, Error::Timeout))) = output else {
panic!("expected recv timeout and buffer");
};
assert_eq!(buf.as_mut_ptr(), pointer);
drop(retired);
let mut read = make_read_request(make_file(), Cache::Enabled);
let pointer = read.buf.as_mut_ptr();
let capacity = read.buf.capacity();
let (output, retired) = Request::ReadAt(read).complete(Err(Error::Timeout));
let RequestOutput::ReadAt(Err((mut buf, Error::Timeout))) = output else {
panic!("expected read timeout and buffer");
};
assert_eq!(buf.as_mut_ptr(), pointer);
assert_eq!(buf.len(), 0);
assert_eq!(buf.capacity(), capacity);
drop(retired);
let request = Request::WriteAt(make_write_request(make_file(), Cache::Enabled));
let (output, retired) = request.complete(Err(Error::Timeout));
assert!(matches!(
output,
RequestOutput::WriteAt(Err(Error::Timeout))
));
drop(retired);
let request = Request::Sync(SyncRequest::new(make_file()));
let (output, retired) = request.complete(Err(Error::Timeout));
assert!(matches!(output, RequestOutput::Sync(Err(Error::Timeout))));
drop(retired);
let request = Request::Connect(make_connect_request("127.0.0.1:1234"));
let (output, retired) = request.complete(Err(Error::Timeout));
assert!(matches!(
output,
RequestOutput::Connect(Err(Error::Timeout))
));
drop(retired);
let request = Request::Poll(make_poll_request());
let (output, retired) = request.complete(Err(Error::Timeout));
assert!(matches!(output, RequestOutput::Poll(Err(Error::Timeout))));
drop(retired);
}
}