use core::future::Future;
use core::pin::Pin;
use core::sync::atomic::{AtomicBool as ExitFlag, AtomicUsize as ExitCount};
use core::task::{Context, Poll};
use std::sync::{Arc, Condvar, Mutex, Once, PoisonError, Weak};
use kovan_queue::seg_queue::SegQueue;
use super::runtime::Inner;
use crate::sync::atomic_waker::AtomicWaker;
use crate::sync::internal::{AtomicBool, AtomicUsize, Ordering, fence};
pub(crate) fn time() -> std::time::Duration {
super::runtime::io_knobs::current().linger
}
pub(crate) type Work = Pin<Box<dyn Future<Output = ()> + Send>>;
pub(crate) fn hand<F>(work: F)
where
F: Future<Output = ()> + Send + 'static,
{
let work: Work = Box::pin(work);
match super::runtime::running_inner() {
Some(inner) => inner.take_linger(work),
None => alone(work),
}
}
pub(crate) fn hand_file<F>(work: F)
where
F: Future<Output = ()> + Send + 'static,
{
HOOKED.call_once(|| {
if unsafe { atexit(wait_at_exit) } != 0 {
log::warn!(
"the process could not be set to wait at its exit for files dropped with bytes written behind"
);
}
});
FILES_OUT.fetch_add(1, Ordering::SeqCst);
hand(Tracked(work));
}
unsafe extern "C" {
fn atexit(callback: extern "C" fn()) -> core::ffi::c_int;
}
static HOOKED: Once = Once::new();
static FILES_OUT: ExitCount = ExitCount::new(0);
static EXITING: ExitFlag = ExitFlag::new(false);
static EXIT_LOCK: Mutex<()> = Mutex::new(());
static EXIT_WAKE: Condvar = Condvar::new();
struct Tracked<F>(F);
impl<F> Future for Tracked<F>
where
F: Future<Output = ()>,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
unsafe { self.map_unchecked_mut(|this| &mut this.0) }.poll(cx)
}
}
impl<F> Drop for Tracked<F> {
fn drop(&mut self) {
if FILES_OUT.fetch_sub(1, Ordering::SeqCst) == 1 && EXITING.load(Ordering::SeqCst) {
let _held = EXIT_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
EXIT_WAKE.notify_all();
}
}
}
extern "C" fn wait_at_exit() {
let bound = super::runtime::exit_linger();
let mut held = EXIT_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
EXITING.store(true, Ordering::SeqCst);
let deadline = std::time::Instant::now() + bound;
loop {
let out = FILES_OUT.load(Ordering::SeqCst);
if out == 0 {
return;
}
let now = std::time::Instant::now();
if now >= deadline {
crate::rt::counters::write_behind_lost(out as u64);
log::warn!(
"{out} dropped files still had bytes written behind when the process exited, after waiting {bound:?}: those bytes were not written"
);
return;
}
held = EXIT_WAKE
.wait_timeout(held, deadline - now)
.unwrap_or_else(PoisonError::into_inner)
.0;
}
}
pub(crate) struct Lingering {
list: SegQueue<Work>,
active: AtomicUsize,
waker: AtomicWaker,
closing: AtomicBool,
over: AtomicBool,
}
impl Lingering {
pub(crate) fn new() -> Self {
Self {
list: SegQueue::new(),
active: AtomicUsize::new(0),
waker: AtomicWaker::new(),
closing: AtomicBool::new(false),
over: AtomicBool::new(false),
}
}
pub(crate) fn active(&self) -> usize {
self.active.load(Ordering::SeqCst)
}
fn ended(&self) {
self.active.fetch_sub(1, Ordering::SeqCst);
self.waker.wake();
}
pub(crate) fn nudge(&self) {
self.waker.wake();
}
pub(crate) fn close(&self) {
self.closing.store(true, Ordering::SeqCst);
}
pub(crate) fn is_closing(&self) -> bool {
self.closing.load(Ordering::SeqCst)
}
pub(crate) fn is_over(&self) -> bool {
self.over.load(Ordering::SeqCst)
}
pub(crate) fn push(&self, work: Work) {
self.list.push(work);
self.waker.wake();
fence(Ordering::SeqCst);
if self.over.load(Ordering::SeqCst) {
self.run_rest_alone();
}
}
pub(crate) fn end(&self) {
self.over.store(true, Ordering::SeqCst);
fence(Ordering::SeqCst);
self.run_rest_alone();
}
fn run_rest_alone(&self) {
while let Ok(work) = self.list.try_pop() {
self.ended();
alone(work);
}
}
}
pub(crate) struct RunAll<'a> {
inner: &'a Inner,
running: Vec<Work>,
}
impl Future for RunAll<'_> {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let this = self.get_mut();
let lingering = this.inner.lingering();
lingering.waker.register(cx.waker());
loop {
while let Ok(work) = lingering.list.try_pop() {
this.running.push(work);
}
this.running.retain_mut(|work| {
let ended = work.as_mut().poll(cx).is_ready();
if ended {
lingering.ended();
}
!ended
});
if lingering.list.is_empty() {
break;
}
}
if this.inner.lingers_done() {
Poll::Ready(())
} else {
Poll::Pending
}
}
}
impl Drop for RunAll<'_> {
fn drop(&mut self) {
let lingering = self.inner.lingering();
for work in self.running.drain(..) {
lingering.ended();
alone(work);
}
}
}
impl Inner {
pub(crate) fn lingers_done(&self) -> bool {
self.workers.shards().all(|shard| shard.settled()) && self.lingering().active() == 0
}
pub(crate) fn run_lingers(&self) -> RunAll<'_> {
RunAll {
inner: self,
running: Vec::new(),
}
}
pub(crate) fn take_linger(self: &Arc<Self>, work: Work) {
self.lingering().active.fetch_add(1, Ordering::SeqCst);
self.route_linger(work);
}
fn route_linger(self: &Arc<Self>, work: Work) {
let lingering = self.lingering();
if !lingering.is_closing() {
let guarded = Guarded {
work: Some(work),
owner: Arc::downgrade(self),
};
return super::runtime::spawn_linger(self, guarded);
}
lingering.push(work);
}
}
pub(crate) struct Guarded {
work: Option<Work>,
owner: Weak<Inner>,
}
impl Future for Guarded {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let this = self.get_mut();
let Some(work) = this.work.as_mut() else {
return Poll::Ready(());
};
let poll = work.as_mut().poll(cx);
if poll.is_ready() {
this.work = None;
if let Some(inner) = this.owner.upgrade() {
inner.lingering().ended();
}
}
poll
}
}
impl Drop for Guarded {
fn drop(&mut self) {
let Some(work) = self.work.take() else {
return;
};
match self.owner.upgrade() {
Some(inner) => inner.route_linger(work),
None => alone(work),
}
}
}
pub(crate) fn on_thread(linger: Guarded) {
alone(Box::pin(OnThread(linger)));
}
struct OnThread(Guarded);
impl Future for OnThread {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let guarded = &mut self.get_mut().0;
let owner = guarded.owner.upgrade();
let _runtime = owner.as_ref().map(super::context::set_runtime);
Pin::new(guarded).poll(cx)
}
}
pub(crate) fn alone(work: Work) {
let unsent = match linger_thread() {
Some(thread) => thread.send(work).err().map(|unsent| unsent.0),
None => Some(work),
};
if let Some(work) = unsent {
run_with_own_runtime(work);
}
}
fn linger_thread() -> Option<&'static crate::sync::mpsc::UnboundedSender<Work>> {
static THREAD: std::sync::OnceLock<crate::sync::mpsc::UnboundedSender<Work>> =
std::sync::OnceLock::new();
if let Some(thread) = THREAD.get() {
return Some(thread);
}
let (send, mut works) = crate::sync::mpsc::unbounded_channel::<Work>();
let started = std::thread::Builder::new()
.name("nuclei-linger".into())
.spawn(move || {
run_with_own_runtime(Box::pin(async move {
while let Some(work) = works.recv().await {
crate::spawn(work).detach();
}
}));
});
match started {
Ok(_) => {
let _ = THREAD.set(send);
THREAD.get()
}
Err(err) => {
log::warn!(
"no thread could start to deliver the bytes a dropped handle left ({err}): they are delivered on the thread that dropped it"
);
None
}
}
}
fn run_with_own_runtime(work: Work) {
match crate::Runtime::builder().worker_threads(0).build() {
Ok(rt) => rt.block_on(work),
Err(err) => log::error!(
"the bytes a dropped handle left were not delivered: no runtime could start for them ({err})"
),
}
}