use std::future::Future;
use std::sync::mpsc::{Receiver, TryRecvError};
use std::time::Duration;
use crate::agent_cx::AgentCx;
struct Abandon<F: FnOnce()> {
callback: Option<F>,
owner: AgentCx,
}
impl<F: FnOnce()> Drop for Abandon<F> {
fn drop(&mut self) {
if let Some(callback) = self.callback.take() {
let _owner = self.owner.cx().clone().set_current_restricted();
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback)).is_err() {
tracing::warn!("request abandonment callback panicked during cleanup");
}
}
}
}
pub fn await_completion<T>(
rx: Receiver<T>,
timeout: Duration,
on_abandon: impl FnOnce(),
) -> impl Future<Output = std::result::Result<T, CompletionWaitError>> {
let owner = AgentCx::for_current_or_request();
let mut abandonment = Abandon {
callback: Some(on_abandon),
owner: owner.clone(),
};
let start = owner
.cx()
.timer_driver()
.map_or_else(asupersync::time::wall_now, |timer| timer.now());
let poll_owner = owner.clone();
async move {
poll_owner
.with_current(async move {
loop {
if owner.checkpoint().is_err() {
return Err(CompletionWaitError::Cancelled);
}
let now = owner
.cx()
.timer_driver()
.map_or_else(asupersync::time::wall_now, |timer| timer.now());
let elapsed = Duration::from_nanos(now.duration_since(start));
if elapsed >= timeout {
return Err(CompletionWaitError::Timeout);
}
match rx.try_recv() {
Ok(value) => {
if owner.checkpoint().is_err() {
return Err(CompletionWaitError::Cancelled);
}
let finished = owner
.cx()
.timer_driver()
.map_or_else(asupersync::time::wall_now, |timer| timer.now());
if Duration::from_nanos(finished.duration_since(start)) >= timeout {
return Err(CompletionWaitError::Timeout);
}
drop(abandonment.callback.take());
return Ok(value);
}
Err(TryRecvError::Disconnected) => return Err(CompletionWaitError::Closed),
Err(TryRecvError::Empty) => {}
}
let delay = timeout
.saturating_sub(elapsed)
.min(Duration::from_millis(10));
asupersync::time::sleep(now, delay).await;
}
})
.await
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionWaitError {
Timeout,
Cancelled,
Closed,
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
fn once<T>(future: impl Future<Output = T>) -> T {
let mut future = std::pin::pin!(future);
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
match future.as_mut().poll(&mut cx) {
Poll::Ready(result) => result,
Poll::Pending => panic!("test completion must be immediately ready"),
}
}
#[test]
fn dropping_an_unpolled_wait_retires_the_request_once() {
let (_sender, receiver) = std::sync::mpsc::sync_channel::<()>(1);
let count = Arc::new(AtomicUsize::new(0));
let callback_count = Arc::clone(&count);
let future = await_completion(receiver, Duration::from_secs(60), move || {
callback_count.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(count.load(Ordering::SeqCst), 0);
drop(future);
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn successful_completion_disarms_abandonment() {
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
sender.send("completed").expect("send");
let count = AtomicUsize::new(0);
let result = once(await_completion(receiver, Duration::from_secs(60), || {
count.fetch_add(1, Ordering::SeqCst);
}));
assert_eq!(result, Ok("completed"));
assert_eq!(count.load(Ordering::SeqCst), 0);
}
#[test]
fn deadline_wins_over_a_buffered_response_and_cleans_up_once() {
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
sender.send("stale").expect("send");
let count = AtomicUsize::new(0);
let result = once(await_completion(receiver, Duration::ZERO, || {
count.fetch_add(1, Ordering::SeqCst);
}));
assert_eq!(result, Err(CompletionWaitError::Timeout));
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn disconnected_sender_still_retires_pending_state() {
let (sender, receiver) = std::sync::mpsc::sync_channel::<()>(1);
drop(sender);
let count = AtomicUsize::new(0);
assert_eq!(
once(await_completion(receiver, Duration::from_secs(60), || {
count.fetch_add(1, Ordering::SeqCst);
})),
Err(CompletionWaitError::Closed)
);
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn cancellation_and_cleanup_retain_the_construction_owner() {
let parent =
AgentCx::for_request_with_budget(asupersync::Budget::new().with_poll_quota(23));
let _parent = parent.cx().clone().set_current_restricted();
let owner = AgentCx::for_request_with_budget(asupersync::Budget::new().with_poll_quota(7));
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
sender.send("stale").expect("send");
let count = AtomicUsize::new(0);
let future = {
let _current = owner.cx().clone().set_current_restricted();
await_completion(receiver, Duration::from_secs(60), || {
assert_eq!(
asupersync::Cx::current().expect("cleanup owner").budget(),
owner.budget()
);
count.fetch_add(1, Ordering::SeqCst);
})
};
owner.cancel_with(asupersync::types::CancelKind::User, Some("owner cancelled"));
assert_eq!(once(future), Err(CompletionWaitError::Cancelled));
assert_eq!(count.load(Ordering::SeqCst), 1);
assert_eq!(
asupersync::Cx::current().expect("parent restored").budget(),
parent.budget()
);
assert!(!parent.is_cancel_requested());
}
#[test]
fn dropping_a_suspended_wait_retires_it_without_another_poll() {
let runtime = asupersync::runtime::RuntimeBuilder::current_thread()
.build()
.expect("native runtime");
runtime.block_on(async {
let (_sender, receiver) = std::sync::mpsc::sync_channel::<()>(1);
let count = AtomicUsize::new(0);
let mut future = Box::pin(await_completion(receiver, Duration::from_secs(60), || {
count.fetch_add(1, Ordering::SeqCst);
}));
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
assert!(future.as_mut().poll(&mut cx).is_pending());
assert_eq!(count.load(Ordering::SeqCst), 0);
drop(future);
assert_eq!(count.load(Ordering::SeqCst), 1);
});
}
}