use std::io;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use kovan_queue::seg_queue::SegQueue;
use super::super::{IoOp, Shared};
use super::{FILE_TAG, FileOp, IGNORED, RecvGroup, RingMem, RingOp, ShardRing, Sqe};
use crate::io::source::ops::{Done, Outcome};
use crate::sync::oneshot;
pub(super) const SHUTDOWN_WAIT: Duration = Duration::from_secs(1);
#[cfg(test)]
std::thread_local! {
pub(crate) static SKIP_CANCEL_FOR_TEST: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
pub(crate) static FILE_WAIT_FOR_TEST: core::cell::Cell<Option<Duration>> = const { core::cell::Cell::new(None) };
static LEAKED_HERE: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn leaked() -> usize {
LEAKED_HERE.with(core::cell::Cell::get)
}
pub(super) fn file_wait() -> Duration {
#[cfg(test)]
if let Some(wait) = FILE_WAIT_FOR_TEST.with(core::cell::Cell::get) {
return wait;
}
crate::rt::linger::time()
}
fn say_given_up(sources: usize, files: usize, linger: Duration) {
let count = (sources + files) as u64;
if count == 0 {
return;
}
crate::rt::counters::operations_given_up(count);
#[cfg(test)]
LEAKED_HERE.with(|here| here.set(here.get() + sources + files));
let mut said = format!("a shard went with {sources} socket operations still out");
if files > 0 {
said = format!(
"a shard went with {files} file calls the kernel had not ended within {linger:?} \
and {sources} socket operations still out"
);
}
let advice = if files > 0 {
", raise write_behind_linger to wait longer"
} else {
""
};
log::warn!(
"{said}: they were given up, so writes among them may not have landed; their memory is kept{advice}"
);
}
struct KeptForGood(#[expect(dead_code, reason = "only kept, never read")] Box<dyn core::any::Any>);
unsafe impl Send for KeptForGood {}
static KEPT: OnceLock<SegQueue<KeptForGood>> = OnceLock::new();
pub(super) fn keep_for_good(what: Box<dyn core::any::Any>) {
KEPT.get_or_init(SegQueue::new).push(KeptForGood(what));
}
fn given_up() -> io::Error {
io::Error::other("the shard went before the kernel ended the operation")
}
fn forsake(op: RingOp) {
let RingOp {
source,
epoch,
read,
mem,
..
} = op;
let ended = matches!(mem, RingMem::ZcHeld(_));
keep_for_good(Box::new(mem));
if ended {
return;
}
source.op_done(
Done {
outcome: Outcome::Forsaken(given_up()),
epoch,
read,
},
None,
);
}
impl ShardRing {
pub(crate) fn cancel_all(&self) {
let waiting: Vec<u64> = self.room.borrow_mut().drain(..).map(|(u, _)| u).collect();
for user_data in waiting {
let op = self.file_ops.borrow_mut().remove(user_data & !FILE_TAG);
if let Some(op) = op {
drop(op.complete(-libc::ECANCELED));
}
}
#[cfg(test)]
if SKIP_CANCEL_FOR_TEST.with(core::cell::Cell::get) {
return;
}
if self.in_flight() > 0 {
self.poller
.borrow_mut()
.push(Sqe::async_cancel_any(true).user_data(IGNORED));
}
}
pub(crate) fn shut_down(&self) -> usize {
self.shut_down_from(std::time::Instant::now(), || {})
}
pub(crate) fn shut_down_from(
&self,
started: std::time::Instant,
mut between: impl FnMut(),
) -> usize {
self.cancel_all();
self.settle_until(started + SHUTDOWN_WAIT, &mut between);
let (sources, freed) = self.forget_sources();
let linger = file_wait();
self.settle_files(started + linger, &mut between);
let files = self.forget_files();
say_given_up(sources, files, linger);
freed
}
pub(crate) fn settle(&self, between: impl FnMut()) {
self.settle_until(std::time::Instant::now() + SHUTDOWN_WAIT, between);
}
fn settle_until(&self, deadline: std::time::Instant, between: impl FnMut()) {
self.reap_until(deadline, between, || self.in_flight() > 0);
}
fn settle_files(&self, deadline: std::time::Instant, between: impl FnMut()) {
self.reap_until(deadline, between, || self.file_ops.borrow().len() > 0);
}
fn reap_until(
&self,
deadline: std::time::Instant,
mut between: impl FnMut(),
out: impl Fn() -> bool,
) {
loop {
between();
if !out() {
return;
}
let left = deadline.saturating_duration_since(std::time::Instant::now());
if left.is_zero()
|| self
.wait(None, Some(left.min(Duration::from_millis(10))))
.is_err()
{
return;
}
}
}
pub(super) fn forget_stuck(&self) -> usize {
let (sources, freed) = self.forget_sources();
let files = self.forget_files();
say_given_up(sources, files, file_wait());
freed
}
fn forget_sources(&self) -> (usize, usize) {
self.recover.keep_sources();
let stuck: Vec<RingOp> = self.ops.borrow_mut().drain().collect();
let stuck_exits: Vec<_> = self.exit_ops.borrow_mut().drain().collect();
let given_up_on = stuck.len() + stuck_exits.len();
let receiving = stuck.iter().any(|op| {
matches!(
op.mem,
RingMem::RecvMulti { .. } | RingMem::RecvFromMulti { .. }
)
});
stuck.into_iter().for_each(forsake);
if !stuck_exits.is_empty() {
keep_for_good(Box::new(stuck_exits));
}
let group = self.recv.borrow_mut().take();
if receiving {
keep_for_good(Box::new(group));
return (given_up_on, 0);
}
let freed = group.as_ref().map_or(0, RecvGroup::reservation);
drop(group);
(given_up_on, freed)
}
fn forget_files(&self) -> usize {
let stuck: Vec<FileOp> = self.file_ops.borrow_mut().drain().collect();
let n = stuck.len();
for file in stuck {
keep_for_good(Box::new(file.forsake(given_up())));
}
n
}
}
struct Kept {
ring: ShardRing,
shared: Arc<Shared>,
}
unsafe impl Send for Kept {}
pub(crate) fn hand_off(ring: ShardRing, shared: Arc<Shared>) {
if !ring.reapable() {
ring.shut_down();
shared.close_io();
return;
}
let kept = Kept { ring, shared };
if let Some(pool) = crate::rt::runtime::blocking_pool_inside() {
pool.spawn(move || kept.keep()).detach();
return;
}
let (give, take) = oneshot::channel::<Kept>();
let started = std::thread::Builder::new()
.name("nuclei-ring-keeper".into())
.spawn(move || {
if let Ok(kept) = take.blocking_recv() {
kept.keep();
}
});
let unkept = match started {
Ok(_) => give.send(kept).err(),
Err(_) => Some(kept),
};
if let Some(kept) = unkept {
kept.ring.shut_down();
kept.shared.close_io();
}
}
impl Kept {
fn keep(self) {
let Kept { ring, shared } = self;
while ring.in_flight() > 0 {
apply_mailbox(&ring, &shared);
if !shared.park.begin_park(|| shared.io_ops.holds_any()) {
continue;
}
let waited = ring.wait(None, None);
shared.park.end_park();
if waited.is_err() {
ring.forget_stuck();
break;
}
}
drop(ring);
shared.close_io();
}
}
fn apply_mailbox(ring: &ShardRing, shared: &Shared) {
shared.io_ops.take_all(|op| match op {
IoOp::RingDetach(slot, out, lease) => ring.detach(slot, out, lease),
IoOp::RingCancel(user_data) => {
ring.cancel(user_data);
}
other => other.gone(),
});
}