#![allow(clippy::arithmetic_side_effects)]
use {
crate::{
file_io::FileCreator,
io_uring::{
memory::{FixedIoBuffer, LargeBuffer},
IO_PRIO_BE_HIGHEST,
},
},
agave_io_uring::{Completion, FixedSlab, Ring, RingOp},
core::slice,
io_uring::{opcode, squeue, types, IoUring},
libc::{O_CREAT, O_NOATIME, O_NOFOLLOW, O_TRUNC, O_WRONLY},
smallvec::SmallVec,
std::{
collections::VecDeque,
ffi::CString,
fs::File,
io::{self, Read},
mem,
os::fd::AsRawFd,
path::PathBuf,
pin::Pin,
sync::Arc,
time::Duration,
},
};
pub const DEFAULT_WRITE_SIZE: usize = 512 * 1024;
type BacklogVec = SmallVec<[PendingWrite; 8 * 1024 * 1024 / DEFAULT_WRITE_SIZE]>;
const MAX_OPEN_FILES: usize = 512;
const MAX_IOWQ_WORKERS: u32 = 4;
const CHECK_PROGRESS_AFTER_SUBMIT_TIMEOUT: Option<Duration> = Some(Duration::from_millis(10));
pub struct IoUringFileCreator<'a, B = LargeBuffer> {
ring: Ring<FileCreatorState<'a>, FileCreatorOp>,
_backing_buffer: B,
}
impl<'a> IoUringFileCreator<'a, LargeBuffer> {
pub fn with_buffer_capacity<F: FnMut(PathBuf) + 'a>(
buf_size: usize,
file_complete: F,
) -> io::Result<Self> {
Self::with_buffer(
LargeBuffer::new(buf_size),
DEFAULT_WRITE_SIZE,
file_complete,
)
}
}
impl<'a, B: AsMut<[u8]>> IoUringFileCreator<'a, B> {
pub fn with_buffer<F: FnMut(PathBuf) + 'a>(
mut buffer: B,
write_capacity: usize,
file_complete: F,
) -> io::Result<Self> {
let ring_qsize = (buffer.as_mut().len() / write_capacity / 2).max(1) as u32;
let ring = IoUring::builder().build(ring_qsize)?;
ring.submitter()
.register_iowq_max_workers(&mut [MAX_IOWQ_WORKERS, 1])?;
Self::with_buffer_and_ring(ring, buffer, write_capacity, file_complete)
}
fn with_buffer_and_ring<F: FnMut(PathBuf) + 'a>(
ring: IoUring,
mut backing_buffer: B,
write_capacity: usize,
file_complete: F,
) -> io::Result<Self> {
let buffer = backing_buffer.as_mut();
assert!(buffer.len() >= write_capacity);
let write_aligned_buf_len = buffer.len() / write_capacity * write_capacity;
let buffer = &mut buffer[..write_aligned_buf_len];
let buffers = unsafe { FixedIoBuffer::split_buffer_chunks(buffer, write_capacity) };
let state = FileCreatorState::new(buffers.collect(), file_complete);
let ring = Ring::new(ring, state);
unsafe { FixedIoBuffer::register(buffer, &ring)? };
let fds = vec![-1; MAX_OPEN_FILES];
ring.register_files(&fds)?;
Ok(Self {
ring,
_backing_buffer: backing_buffer,
})
}
}
impl<B> FileCreator for IoUringFileCreator<'_, B> {
fn schedule_create_at_dir(
&mut self,
path: PathBuf,
mode: u32,
parent_dir_handle: Arc<File>,
contents: &mut dyn Read,
) -> io::Result<()> {
let file_key = self.open(path, mode, parent_dir_handle)?;
self.write_and_close(contents, file_key)
}
fn file_complete(&mut self, path: PathBuf) {
(self.ring.context_mut().file_complete)(path)
}
fn drain(&mut self) -> io::Result<()> {
let res = self.ring.drain();
self.ring.context().log_stats();
res
}
}
impl<B> IoUringFileCreator<'_, B> {
fn open(&mut self, path: PathBuf, mode: u32, dir_handle: Arc<File>) -> io::Result<usize> {
let file = PendingFile::from_path(path);
let path_cstring = Pin::new(file.path_cstring());
let file_key = self.wait_add_file(file)?;
let op = FileCreatorOp::Open(OpenOp {
dir_handle,
path_cstring,
mode,
file_key,
});
self.ring.push(op)?;
Ok(file_key)
}
fn wait_add_file(&mut self, file: PendingFile) -> io::Result<usize> {
loop {
self.ring.process_completions()?;
if self.ring.context().files.len() < self.ring.context().files.capacity() {
break;
}
self.ring
.submit_and_wait(1, CHECK_PROGRESS_AFTER_SUBMIT_TIMEOUT)?;
}
let file_key = self.ring.context_mut().files.insert(file);
Ok(file_key)
}
fn write_and_close(&mut self, mut src: impl Read, file_key: usize) -> io::Result<()> {
let mut offset = 0;
loop {
let buf = self.wait_free_buf()?;
let state = self.ring.context_mut();
let file = state.files.get_mut(file_key).unwrap();
let mut_slice = unsafe { slice::from_raw_parts_mut(buf.as_mut_ptr(), buf.len()) };
let len = src.read(mut_slice)?;
if len == 0 {
file.eof = true;
state.buffers.push_front(buf);
if file.is_completed() {
(state.file_complete)(mem::take(&mut file.path));
self.ring
.push(FileCreatorOp::Close(CloseOp::new(file_key)))?;
}
break;
}
file.writes_started += 1;
if file.completed_open {
let op = WriteOp {
file_key,
offset,
buf,
buf_offset: 0,
write_len: len,
};
state.submitted_writes_size += len;
self.ring.push(FileCreatorOp::Write(op))?;
} else {
file.backlog.push((buf, offset, len));
}
offset += len;
}
Ok(())
}
fn wait_free_buf(&mut self) -> io::Result<FixedIoBuffer> {
loop {
self.ring.process_completions()?;
let state = self.ring.context_mut();
if let Some(buf) = state.buffers.pop_front() {
return Ok(buf);
}
state.stats.no_buf_count += 1;
state.stats.no_buf_sum_submitted_write_sizes += state.submitted_writes_size;
self.ring
.submit_and_wait(1, CHECK_PROGRESS_AFTER_SUBMIT_TIMEOUT)?;
}
}
}
struct FileCreatorState<'a> {
files: FixedSlab<PendingFile>,
buffers: VecDeque<FixedIoBuffer>,
file_complete: Box<dyn FnMut(PathBuf) + 'a>,
open_fds: usize,
submitted_writes_size: usize,
stats: FileCreatorStats,
}
impl<'a> FileCreatorState<'a> {
fn new(buffers: VecDeque<FixedIoBuffer>, file_complete: impl FnMut(PathBuf) + 'a) -> Self {
Self {
files: FixedSlab::with_capacity(MAX_OPEN_FILES),
buffers,
file_complete: Box::new(file_complete),
open_fds: 0,
submitted_writes_size: 0,
stats: FileCreatorStats::default(),
}
}
fn mark_file_opened(&mut self, file_key: usize) -> BacklogVec {
let file = self.files.get_mut(file_key).unwrap();
file.completed_open = true;
self.open_fds += 1;
if self.buffers.len() * 2 > self.buffers.capacity() {
self.stats.large_buf_headroom_count += 1;
}
mem::take(&mut file.backlog)
}
fn mark_write_completed(
&mut self,
file_key: usize,
write_len: usize,
buf: FixedIoBuffer,
) -> bool {
self.submitted_writes_size -= write_len;
self.buffers.push_front(buf);
let file = self.files.get_mut(file_key).unwrap();
file.writes_completed += 1;
if file.is_completed() {
(self.file_complete)(mem::take(&mut file.path));
return true;
}
false
}
fn mark_file_closed(&mut self, file_key: usize) {
let _ = self.files.remove(file_key);
self.open_fds -= 1;
}
fn log_stats(&self) {
self.stats.log();
}
}
#[derive(Debug, Default)]
struct FileCreatorStats {
large_buf_headroom_count: u32,
no_buf_count: u32,
no_buf_sum_submitted_write_sizes: usize,
}
impl FileCreatorStats {
fn log(&self) {
let avg_writes_at_no_buf = self
.no_buf_sum_submitted_write_sizes
.checked_div(self.no_buf_count as usize)
.unwrap_or_default();
log::info!(
"files creation stats - large buf headroom: {}, no buf count: {}, avg pending writes \
at no buf: {avg_writes_at_no_buf}",
self.large_buf_headroom_count,
self.no_buf_count,
);
}
}
#[derive(Debug)]
struct OpenOp {
dir_handle: Arc<File>,
path_cstring: Pin<CString>,
mode: libc::mode_t,
file_key: usize,
}
impl OpenOp {
fn entry(&mut self) -> squeue::Entry {
let at_dir_fd = types::Fd(self.dir_handle.as_raw_fd());
opcode::OpenAt::new(at_dir_fd, self.path_cstring.as_ptr() as _)
.flags(O_CREAT | O_TRUNC | O_NOFOLLOW | O_WRONLY | O_NOATIME)
.mode(self.mode)
.file_index(Some(
types::DestinationSlot::try_from_slot_target(self.file_key as u32).unwrap(),
))
.build()
}
fn complete(
&mut self,
ring: &mut Completion<FileCreatorState, FileCreatorOp>,
res: io::Result<i32>,
) -> io::Result<()>
where
Self: Sized,
{
res?;
let backlog = ring.context_mut().mark_file_opened(self.file_key);
for (buf, offset, len) in backlog {
let op = WriteOp {
file_key: self.file_key,
offset,
buf,
buf_offset: 0,
write_len: len,
};
ring.context_mut().submitted_writes_size += len;
ring.push(FileCreatorOp::Write(op));
}
Ok(())
}
}
#[derive(Debug)]
struct CloseOp {
file_key: usize,
}
impl<'a> CloseOp {
fn new(file_key: usize) -> Self {
Self { file_key }
}
fn entry(&mut self) -> squeue::Entry {
opcode::Close::new(types::Fixed(self.file_key as u32)).build()
}
fn complete(
&mut self,
ring: &mut Completion<FileCreatorState<'a>, FileCreatorOp>,
res: io::Result<i32>,
) -> io::Result<()>
where
Self: Sized,
{
let _ = res?;
ring.context_mut().mark_file_closed(self.file_key);
Ok(())
}
}
#[derive(Debug)]
struct WriteOp {
file_key: usize,
offset: usize,
buf: FixedIoBuffer,
buf_offset: usize,
write_len: usize,
}
impl<'a> WriteOp {
fn entry(&mut self) -> squeue::Entry {
let WriteOp {
file_key,
offset,
buf,
buf_offset,
write_len,
} = self;
opcode::WriteFixed::new(
types::Fixed(*file_key as u32),
unsafe { buf.as_mut_ptr().byte_add(*buf_offset) },
*write_len as u32,
buf.io_buf_index()
.expect("should have a valid fixed buffer"),
)
.offset(*offset as u64)
.ioprio(IO_PRIO_BE_HIGHEST)
.build()
}
fn complete(
&mut self,
ring: &mut Completion<FileCreatorState<'a>, FileCreatorOp>,
res: io::Result<i32>,
) -> io::Result<()>
where
Self: Sized,
{
let written = match res {
Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
Ok(res) => res as usize,
Err(err) => return Err(err),
};
let WriteOp {
file_key,
offset,
ref mut buf,
buf_offset,
write_len,
} = self;
let buf = mem::replace(buf, FixedIoBuffer::empty());
let total_written = *buf_offset + written;
if written < *write_len {
log::warn!("short write ({written}/{}), file={}", *write_len, *file_key);
ring.push(FileCreatorOp::Write(WriteOp {
file_key: *file_key,
offset: *offset + written,
buf,
buf_offset: total_written,
write_len: *write_len - written,
}));
return Ok(());
}
if ring
.context_mut()
.mark_write_completed(*file_key, total_written, buf)
{
ring.push(FileCreatorOp::Close(CloseOp::new(*file_key)));
}
Ok(())
}
}
#[derive(Debug)]
enum FileCreatorOp {
Open(OpenOp),
Close(CloseOp),
Write(WriteOp),
}
impl RingOp<FileCreatorState<'_>> for FileCreatorOp {
fn entry(&mut self) -> squeue::Entry {
match self {
Self::Open(op) => op.entry(),
Self::Close(op) => op.entry(),
Self::Write(op) => op.entry(),
}
}
fn complete(
&mut self,
ring: &mut Completion<FileCreatorState, Self>,
res: io::Result<i32>,
) -> io::Result<()>
where
Self: Sized,
{
match self {
Self::Open(op) => op.complete(ring, res),
Self::Close(op) => op.complete(ring, res),
Self::Write(op) => op.complete(ring, res),
}
}
}
type PendingWrite = (FixedIoBuffer, usize, usize);
#[derive(Debug)]
struct PendingFile {
path: PathBuf,
completed_open: bool,
backlog: BacklogVec,
eof: bool,
writes_started: usize,
writes_completed: usize,
}
impl PendingFile {
fn from_path(path: PathBuf) -> Self {
Self {
path,
completed_open: false,
backlog: SmallVec::new(),
writes_started: 0,
writes_completed: 0,
eof: false,
}
}
fn path_cstring(&self) -> CString {
let os_str = self.path.file_name().expect("path must contain filename");
CString::new(os_str.as_encoded_bytes()).expect("path mustn't contain interior NULs")
}
fn is_completed(&self) -> bool {
self.eof && self.writes_started == self.writes_completed
}
}