use std::{
collections::HashMap,
future::Future,
hash::Hash,
pin::Pin,
sync::{
Arc, Weak,
atomic::{AtomicU64, Ordering},
},
task::{Context, Poll},
time::Duration,
};
use parking_lot::Mutex;
use tokio::{
sync::oneshot,
time::{Sleep, sleep},
};
mod error;
pub use error::Error;
use error::Result;
struct Entry<R> {
id: u64,
sender: oneshot::Sender<R>,
}
struct Inner<K: Eq + Hash, R> {
timeout_ms: AtomicU64,
next_id: AtomicU64,
requests: Mutex<HashMap<K, Entry<R>>>,
}
pub struct ResponseWaiter<K: Eq + Hash, R> {
receiver: oneshot::Receiver<R>,
sleep: Pin<Box<Sleep>>,
inner: Weak<Inner<K, R>>,
key: K,
id: u64,
done: bool,
}
impl<K: Eq + Hash, R> Unpin for ResponseWaiter<K, R> {}
impl<K: Eq + Hash, R> ResponseWaiter<K, R> {
fn clear(&mut self) {
if let Some(inner) = self.inner.upgrade() {
let mut guard = inner.requests.lock();
if guard.get(&self.key).is_some_and(|e| e.id == self.id) {
guard.remove(&self.key);
}
}
}
}
impl<K: Eq + Hash, R> Future for ResponseWaiter<K, R> {
type Output = Result<R>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match Pin::new(&mut this.receiver).poll(cx) {
Poll::Ready(Ok(response)) => {
this.done = true;
return Poll::Ready(Ok(response));
}
Poll::Ready(Err(_)) => {
this.done = true;
return Poll::Ready(Err(Error::Canceled));
}
Poll::Pending => {}
}
if this.sleep.as_mut().poll(cx).is_ready() {
this.clear();
this.done = true;
return Poll::Ready(Err(Error::RequestTimeout));
}
Poll::Pending
}
}
impl<K: Eq + Hash, R> Drop for ResponseWaiter<K, R> {
fn drop(&mut self) {
if !self.done {
self.clear();
}
}
}
pub struct PendingRequests<K: Eq + Hash, R> {
inner: Arc<Inner<K, R>>,
}
impl<K: Eq + Hash, R> Clone for PendingRequests<K, R> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
const DEFAULT_TIMEOUT_MS: u64 = 6000;
impl<K: Eq + Hash, R> Default for PendingRequests<K, R> {
fn default() -> Self {
Self {
inner: Arc::new(Inner {
timeout_ms: AtomicU64::new(DEFAULT_TIMEOUT_MS),
next_id: AtomicU64::new(0),
requests: Mutex::new(HashMap::new()),
}),
}
}
}
impl<K: Eq + Hash + Clone, R> PendingRequests<K, R> {
pub fn new() -> Self {
Self::default()
}
pub fn set_timeout(&self, timeout: Duration) {
self.inner
.timeout_ms
.store(timeout.as_millis() as u64, Ordering::Relaxed);
}
pub fn timeout(&self) -> Duration {
Duration::from_millis(self.inner.timeout_ms.load(Ordering::Relaxed))
}
pub fn len(&self) -> usize {
self.inner.requests.lock().len()
}
pub fn is_empty(&self) -> bool {
self.inner.requests.lock().is_empty()
}
pub fn contains(&self, key: &K) -> bool {
self.inner.requests.lock().contains_key(key)
}
pub fn prepare_response(&self, key: K) -> Result<ResponseWaiter<K, R>> {
self.prepare_response_with_timeout(key, self.timeout())
}
pub fn prepare_response_with_timeout(
&self,
key: K,
timeout: Duration,
) -> Result<ResponseWaiter<K, R>> {
let mut guard = self.inner.requests.lock();
if guard.contains_key(&key) {
return Err(Error::KeyAlreadyExists);
}
let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
guard.insert(key.clone(), Entry { id, sender: tx });
Ok(ResponseWaiter {
receiver: rx,
sleep: Box::pin(sleep(timeout)),
inner: Arc::downgrade(&self.inner),
key,
id,
done: false,
})
}
pub fn handle_response(&self, key: K, response: R) -> Result<()> {
let entry = self
.inner
.requests
.lock()
.remove(&key)
.ok_or(Error::KeyNotFound)?;
entry
.sender
.send(response)
.map_err(|_| Error::ReceiverDropped)
}
pub fn cancel(&self, key: &K) -> Result<()> {
self.inner
.requests
.lock()
.remove(key)
.map(|_| ())
.ok_or(Error::KeyNotFound)
}
pub fn cancel_all(&self) -> usize {
let mut guard = self.inner.requests.lock();
let n = guard.len();
guard.clear();
n
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn roundtrip_delivers_response() {
let requests = PendingRequests::<u64, &str>::new();
let waiter = requests.prepare_response(1).unwrap();
requests.handle_response(1, "pong").unwrap();
assert_eq!(waiter.await.unwrap(), "pong");
assert!(requests.is_empty());
}
#[tokio::test]
async fn duplicate_key_is_rejected() {
let requests = PendingRequests::<u64, ()>::new();
let _waiter = requests.prepare_response(1).unwrap();
assert!(matches!(
requests.prepare_response(1),
Err(Error::KeyAlreadyExists)
));
}
#[tokio::test]
async fn unknown_key_response_errors() {
let requests = PendingRequests::<u64, ()>::new();
assert!(matches!(
requests.handle_response(42, ()),
Err(Error::KeyNotFound)
));
}
#[tokio::test(start_paused = true)]
async fn timeout_resolves_and_cleans_up() {
let requests = PendingRequests::<u64, ()>::new();
let waiter = requests
.prepare_response_with_timeout(1, Duration::from_millis(50))
.unwrap();
assert!(matches!(waiter.await, Err(Error::RequestTimeout)));
assert!(requests.is_empty(), "timed-out entry should be removed");
}
#[tokio::test]
async fn cancel_resolves_waiter() {
let requests = PendingRequests::<u64, ()>::new();
let waiter = requests.prepare_response(1).unwrap();
requests.cancel(&1).unwrap();
assert!(matches!(waiter.await, Err(Error::Canceled)));
assert!(matches!(requests.cancel(&1), Err(Error::KeyNotFound)));
}
#[tokio::test]
async fn dropped_waiter_is_reported_and_cleaned() {
let requests = PendingRequests::<u64, ()>::new();
let waiter = requests.prepare_response(1).unwrap();
drop(waiter);
assert!(
requests.is_empty(),
"dropped waiter should remove its entry"
);
assert!(matches!(
requests.handle_response(1, ()),
Err(Error::KeyNotFound)
));
}
#[tokio::test]
async fn dropping_registry_cancels_waiter() {
let requests = PendingRequests::<u64, ()>::new();
let waiter = requests.prepare_response(1).unwrap();
drop(requests);
assert!(matches!(waiter.await, Err(Error::Canceled)));
}
#[tokio::test(start_paused = true)]
async fn reused_key_after_timeout_is_independent() {
let requests = PendingRequests::<u64, &str>::new();
let first = requests
.prepare_response_with_timeout(1, Duration::from_millis(50))
.unwrap();
assert!(matches!(first.await, Err(Error::RequestTimeout)));
let second = requests.prepare_response(1).unwrap();
requests.handle_response(1, "ok").unwrap();
assert_eq!(second.await.unwrap(), "ok");
}
#[tokio::test]
async fn clones_share_state() {
let requests = PendingRequests::<u64, &str>::new();
let other = requests.clone();
let waiter = requests.prepare_response(1).unwrap();
other.handle_response(1, "pong").unwrap();
assert_eq!(waiter.await.unwrap(), "pong");
assert!(other.is_empty());
}
#[tokio::test]
async fn contains_reflects_pending_state() {
let requests = PendingRequests::<u64, ()>::new();
assert!(!requests.contains(&1));
let _waiter = requests.prepare_response(1).unwrap();
assert!(requests.contains(&1));
requests.cancel(&1).unwrap();
assert!(!requests.contains(&1));
}
#[tokio::test]
async fn cancel_all_fails_every_waiter() {
let requests = PendingRequests::<u64, ()>::new();
let a = requests.prepare_response(1).unwrap();
let b = requests.prepare_response(2).unwrap();
assert_eq!(requests.cancel_all(), 2);
assert!(requests.is_empty());
assert!(matches!(a.await, Err(Error::Canceled)));
assert!(matches!(b.await, Err(Error::Canceled)));
}
#[tokio::test]
async fn set_timeout_works_through_shared_handle() {
let requests = PendingRequests::<u64, ()>::new();
let handle = requests.clone();
handle.set_timeout(Duration::from_secs(30));
assert_eq!(requests.timeout(), Duration::from_secs(30));
}
#[tokio::test]
async fn surviving_clone_keeps_waiter_alive() {
let requests = PendingRequests::<u64, &str>::new();
let clone = requests.clone();
let waiter = requests.prepare_response(1).unwrap();
drop(requests);
clone.handle_response(1, "still here").unwrap();
assert_eq!(waiter.await.unwrap(), "still here");
}
}