#[cfg(test)]
use core::cell::Cell;
use core::cell::RefCell;
use std::io;
use std::time::Instant;
use super::exits::ExitOp;
use super::keeper::{SHUTDOWN_WAIT, file_wait, keep_for_good};
use super::{EXIT_TAG, FILE_TAG, IGNORED, RECOVER, SWEEP, ShardRing, counts_as_write};
use crate::fs::ring::FileMem;
use crate::io::source::ops::{Done, Outcome};
use crate::io::source::ring::{RingMem, RingOp};
use crate::rt::shard::Shard;
use crate::syscore::linux::iouring::ring::Sqe;
use crate::syscore::linux::iouring::ring::sys::KernelTimespec;
static SHORT_WAIT: KernelTimespec = KernelTimespec {
tv_sec: SHUTDOWN_WAIT.as_secs() as i64,
tv_nsec: SHUTDOWN_WAIT.subsec_nanos() as i64,
};
#[cfg(test)]
std::thread_local! {
pub(crate) static SWEEP_WAIT_FOR_TEST: Cell<Option<core::time::Duration>> = const { Cell::new(None) };
static TEST_WAIT: Cell<KernelTimespec> = const { Cell::new(KernelTimespec { tv_sec: 0, tv_nsec: 0 }) };
pub(crate) static DROP_NEXT_FOR_TEST: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn dropped_error() -> io::Error {
io::Error::other("the kernel dropped this operation's completion")
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Held {
Source,
File,
Exit,
}
struct Note {
user_data: u64,
held: Held,
due: Instant,
}
struct Recovery {
notes: Vec<Note>,
ended: usize,
}
#[derive(Default)]
pub(super) struct Recover {
under_way: RefCell<Option<Recovery>>,
sources: RefCell<Vec<(u64, RingMem)>>,
files: RefCell<Vec<(u64, FileMem)>>,
exits: RefCell<Vec<(u64, ExitOp)>>,
}
fn stays(mem: &RingMem) -> bool {
matches!(
mem,
RingMem::ZcHeld(_) | RingMem::Zombie | RingMem::RecvZombie { .. }
)
}
impl Recover {
#[cold]
#[inline(never)]
pub(super) fn late(&self, user_data: u64) {
let mut sources = self.sources.borrow_mut();
if let Some(at) = sources.iter().position(|(u, _)| *u == user_data) {
drop(sources.swap_remove(at));
}
}
pub(super) fn keep_sources(&self) {
let sources = core::mem::take(&mut *self.sources.borrow_mut());
if !sources.is_empty() {
keep_for_good(Box::new(sources));
}
}
pub(super) fn keep_leftovers(&self) {
self.keep_sources();
let files = core::mem::take(&mut *self.files.borrow_mut());
if !files.is_empty() {
keep_for_good(Box::new(files));
}
let exits = core::mem::take(&mut *self.exits.borrow_mut());
if !exits.is_empty() {
keep_for_good(Box::new(exits));
}
}
}
impl ShardRing {
fn short_wait() -> core::time::Duration {
#[cfg(test)]
if let Some(wait) = SWEEP_WAIT_FOR_TEST.with(Cell::get) {
return wait;
}
SHUTDOWN_WAIT
}
fn arm_sweep(&self) {
#[cfg(test)]
if let Some(wait) = SWEEP_WAIT_FOR_TEST.with(Cell::get) {
let at = TEST_WAIT.with(|t| {
t.set(KernelTimespec {
tv_sec: wait.as_secs() as i64,
tv_nsec: i64::from(wait.subsec_nanos()),
});
t.as_ptr().cast_const()
});
self.poller
.borrow_mut()
.push(Sqe::timeout(at).user_data(SWEEP));
return;
}
self.poller
.borrow_mut()
.push(Sqe::timeout(&SHORT_WAIT).user_data(SWEEP));
}
#[cold]
#[inline(never)]
fn begin_recovery(&self) {
let now = Instant::now();
let (short, long) = (now + Self::short_wait(), now + file_wait());
let mut found = Vec::new();
{
let mut ops = self.ops.borrow_mut();
let keys: Vec<u64> = ops.keys().collect();
for user_data in keys {
if ops.get_mut(user_data).is_some_and(|op| !stays(&op.mem)) {
found.push(Note {
user_data,
held: Held::Source,
due: short,
});
}
}
}
let mut waiting: Vec<u64> = self.room.borrow().iter().map(|(u, _)| *u).collect();
waiting.sort_unstable();
for key in self.file_ops.borrow().keys() {
let user_data = key | FILE_TAG;
if waiting.binary_search(&user_data).is_err() {
found.push(Note {
user_data,
held: Held::File,
due: long,
});
}
}
for key in self.exit_ops.borrow().keys() {
found.push(Note {
user_data: key | FILE_TAG | EXIT_TAG,
held: Held::Exit,
due: short,
});
}
let mut under_way = self.recover.under_way.borrow_mut();
let recovery = under_way.get_or_insert_with(|| Recovery {
notes: Vec::new(),
ended: 0,
});
recovery.notes.sort_unstable_by_key(|n| n.user_data);
found.retain(|n| {
recovery
.notes
.binary_search_by_key(&n.user_data, |m| m.user_data)
.is_err()
});
if found.is_empty() {
return;
}
let noted = found.len();
recovery.notes.append(&mut found);
drop(under_way);
#[cfg(test)]
let cancel = !super::keeper::SKIP_CANCEL_FOR_TEST.with(Cell::get);
#[cfg(not(test))]
let cancel = true;
if cancel {
self.poller
.borrow_mut()
.push(Sqe::async_cancel_any(true).user_data(IGNORED));
}
self.arm_sweep();
log::warn!(
"the kernel dropped io_uring completions: {noted} operations were cancelled, and those that do not end will be ended with an error"
);
}
#[cold]
#[inline(never)]
fn sweep(&self, shard: Option<&Shard>, res: i32) {
if res == -libc::ECANCELED {
return;
}
let now = Instant::now();
let due = {
let mut under_way = self.recover.under_way.borrow_mut();
let Some(recovery) = under_way.as_mut() else {
return;
};
let (due, rest): (Vec<Note>, Vec<Note>) = core::mem::take(&mut recovery.notes)
.into_iter()
.partition(|n| n.due <= now);
recovery.notes = rest;
due
};
let mut ended = 0;
for note in due {
ended += usize::from(match note.held {
Held::Source => self.sweep_source(shard, note.user_data),
Held::File => self.sweep_file(note.user_data),
Held::Exit => self.sweep_exit(note.user_data),
});
}
let mut under_way = self.recover.under_way.borrow_mut();
let Some(recovery) = under_way.as_mut() else {
return;
};
recovery.ended += ended;
if !recovery.notes.is_empty() {
drop(under_way);
self.arm_sweep();
return;
}
let total = recovery.ended;
*under_way = None;
drop(under_way);
if total > 0 {
log::warn!(
"the kernel dropped io_uring completions: {total} operations did not end and were ended with an error"
);
}
}
fn sweep_source(&self, shard: Option<&Shard>, user_data: u64) -> bool {
let (op, write) = {
let mut ops = self.ops.borrow_mut();
if ops.get_mut(user_data).is_none_or(|op| stays(&op.mem)) {
return false;
}
let Some(op) = ops.remove(user_data) else {
return false;
};
let write = counts_as_write(&op);
(op, write)
};
let RingOp {
source,
file,
epoch,
read,
mem,
} = op;
self.recover.sources.borrow_mut().push((user_data, mem));
if write {
self.writes.set(self.writes.get().saturating_sub(1));
}
self.files.borrow_mut().done(file);
source.op_done(
Done {
outcome: Outcome::Forsaken(dropped_error()),
epoch,
read,
},
shard,
);
true
}
fn sweep_file(&self, user_data: u64) -> bool {
let Some(op) = self.file_ops.borrow_mut().remove(user_data & !FILE_TAG) else {
return false;
};
self.in_kernel.set(self.in_kernel.get().saturating_sub(1));
self.admit_waiting();
let mem = op.forsake(dropped_error());
self.recover.files.borrow_mut().push((user_data, mem));
true
}
fn sweep_exit(&self, user_data: u64) -> bool {
let Some(op) = self.take_exit(user_data) else {
return false;
};
op.finish_with(libc::EIO);
self.recover.exits.borrow_mut().push((user_data, op));
true
}
#[cold]
#[inline(never)]
pub(super) fn swept_file_ended(&self, user_data: u64) {
let mut files = self.recover.files.borrow_mut();
if let Some(at) = files.iter().position(|(u, _)| *u == user_data) {
#[cfg(feature = "io-stats")]
if self.timing.get().is_some() {
self.timed(user_data, None, 0);
}
drop(files.swap_remove(at));
}
}
#[cold]
#[inline(never)]
pub(super) fn exit_unknown(&self, shard: Option<&Shard>, user_data: u64, res: i32) {
match user_data {
RECOVER => self.begin_recovery(),
SWEEP => self.sweep(shard, res),
_ => {
let mut exits = self.recover.exits.borrow_mut();
if let Some(at) = exits.iter().position(|(u, _)| *u == user_data) {
drop(exits.swap_remove(at));
}
}
}
}
}
#[cfg(test)]
impl ShardRing {
pub(super) fn drops_for_test(
&self,
cqe: &crate::syscore::linux::iouring::ring::IoUringCqe,
) -> bool {
use crate::syscore::linux::iouring::ring::sys::IORING_CQE_F_MORE;
if cqe.user_data & FILE_TAG != 0
|| cqe.res <= 0
|| cqe.flags & IORING_CQE_F_MORE != 0
|| !DROP_NEXT_FOR_TEST.with(Cell::get)
|| self.ops.borrow_mut().get_mut(cqe.user_data).is_none()
{
return false;
}
DROP_NEXT_FOR_TEST.with(|drop| drop.set(false));
self.poller.borrow_mut().drop_for_test();
true
}
pub(crate) fn recover_for_test(&self) {
self.poller.borrow_mut().ask_recovery();
}
pub(crate) fn recovering(&self) -> bool {
self.recover.under_way.borrow().is_some()
}
pub(crate) fn swept_sources(&self) -> usize {
self.recover.sources.borrow().len()
}
pub(crate) fn swept_files(&self) -> usize {
self.recover.files.borrow().len()
}
pub(crate) fn table_len_for_test(&self) -> usize {
self.ops.borrow().len()
}
}