Skip to main content

pending_requests/
lib.rs

1//! Track in-flight requests and await their responses by key.
2//!
3//! A [`PendingRequests`] registry lets one task register a request under a
4//! key, hand back a [`ResponseWaiter`] future, and have another task deliver
5//! the matching response later. This is the classic pattern behind
6//! request/response protocols multiplexed over a single connection.
7//!
8//! ```
9//! # use std::time::Duration;
10//! # use pending_requests::PendingRequests;
11//! # #[tokio::main]
12//! # async fn main() {
13//! let requests = PendingRequests::<u64, String>::new();
14//! let waiter = requests.prepare_response(1).unwrap();
15//!
16//! // ... elsewhere, when the response for key `1` arrives:
17//! requests.handle_response(1, "pong".to_string()).unwrap();
18//!
19//! assert_eq!(waiter.await.unwrap(), "pong");
20//! # }
21//! ```
22
23use std::{
24    collections::HashMap,
25    future::Future,
26    hash::Hash,
27    pin::Pin,
28    sync::{
29        Arc, Weak,
30        atomic::{AtomicU64, Ordering},
31    },
32    task::{Context, Poll},
33    time::Duration,
34};
35
36use parking_lot::Mutex;
37use tokio::{
38    sync::oneshot,
39    time::{Sleep, sleep},
40};
41
42mod error;
43
44pub use error::Error;
45use error::Result;
46
47/// One stored slot in the registry. The `id` distinguishes successive
48/// requests that reuse the same key, so a stale waiter never removes an
49/// entry that belongs to a newer request.
50struct Entry<R> {
51    id: u64,
52    sender: oneshot::Sender<R>,
53}
54
55/// Shared state behind the registry. Cloning a [`PendingRequests`] clones the
56/// `Arc` to this, so every clone observes the same pending requests, timeout,
57/// and id counter.
58struct Inner<K: Eq + Hash, R> {
59    /// Default timeout in milliseconds, mutable at runtime via an atomic so
60    /// it can be changed through a shared `&self`.
61    timeout_ms: AtomicU64,
62    next_id: AtomicU64,
63    requests: Mutex<HashMap<K, Entry<R>>>,
64}
65
66/// A future that resolves with the response for a single request.
67///
68/// Resolves to:
69/// - `Ok(response)` when [`PendingRequests::handle_response`] delivers a value,
70/// - `Err(Error::RequestTimeout)` when the configured timeout elapses first,
71/// - `Err(Error::Canceled)` when the request is canceled or the registry is dropped.
72pub struct ResponseWaiter<K: Eq + Hash, R> {
73    receiver: oneshot::Receiver<R>,
74    sleep: Pin<Box<Sleep>>,
75    inner: Weak<Inner<K, R>>,
76    key: K,
77    id: u64,
78    /// Set once the future has resolved, so `Drop` skips cleanup.
79    done: bool,
80}
81
82/// The `Sleep` is the only `!Unpin` field and it is kept behind a
83/// `Pin<Box<_>>`; nothing in this struct is self-referential, so it is safe
84/// to treat the waiter itself as `Unpin` regardless of `K`/`R`.
85impl<K: Eq + Hash, R> Unpin for ResponseWaiter<K, R> {}
86
87impl<K: Eq + Hash, R> ResponseWaiter<K, R> {
88    /// Remove this waiter's entry from the registry, but only if it is still
89    /// the same generation we registered (guards against a reused key).
90    fn clear(&mut self) {
91        if let Some(inner) = self.inner.upgrade() {
92            let mut guard = inner.requests.lock();
93            if guard.get(&self.key).is_some_and(|e| e.id == self.id) {
94                guard.remove(&self.key);
95            }
96        }
97    }
98}
99
100impl<K: Eq + Hash, R> Future for ResponseWaiter<K, R> {
101    type Output = Result<R>;
102
103    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
104        // All fields are `Unpin` (the `Sleep` lives behind a `Pin<Box<_>>`),
105        // so we can work with a plain mutable reference.
106        let this = self.get_mut();
107
108        // A response (or a closed channel) takes priority over the timeout.
109        match Pin::new(&mut this.receiver).poll(cx) {
110            Poll::Ready(Ok(response)) => {
111                this.done = true;
112                return Poll::Ready(Ok(response));
113            }
114            Poll::Ready(Err(_)) => {
115                // Sender was dropped without sending: canceled or registry gone.
116                this.done = true;
117                return Poll::Ready(Err(Error::Canceled));
118            }
119            Poll::Pending => {}
120        }
121
122        if this.sleep.as_mut().poll(cx).is_ready() {
123            this.clear();
124            this.done = true;
125            return Poll::Ready(Err(Error::RequestTimeout));
126        }
127
128        Poll::Pending
129    }
130}
131
132impl<K: Eq + Hash, R> Drop for ResponseWaiter<K, R> {
133    fn drop(&mut self) {
134        if !self.done {
135            self.clear();
136        }
137    }
138}
139
140/// A registry of in-flight requests awaiting responses, keyed by `K`.
141///
142/// Cloning is cheap and shares all state: register a request on one clone and
143/// deliver its response from another. This is the expected way to use the
144/// registry across tasks (e.g. a reader task calling [`handle_response`] while
145/// many sender tasks await their [`ResponseWaiter`]s).
146///
147/// [`handle_response`]: PendingRequests::handle_response
148pub struct PendingRequests<K: Eq + Hash, R> {
149    inner: Arc<Inner<K, R>>,
150}
151
152impl<K: Eq + Hash, R> Clone for PendingRequests<K, R> {
153    fn clone(&self) -> Self {
154        Self {
155            inner: Arc::clone(&self.inner),
156        }
157    }
158}
159
160const DEFAULT_TIMEOUT_MS: u64 = 6000;
161
162impl<K: Eq + Hash, R> Default for PendingRequests<K, R> {
163    fn default() -> Self {
164        Self {
165            inner: Arc::new(Inner {
166                timeout_ms: AtomicU64::new(DEFAULT_TIMEOUT_MS),
167                next_id: AtomicU64::new(0),
168                requests: Mutex::new(HashMap::new()),
169            }),
170        }
171    }
172}
173
174impl<K: Eq + Hash + Clone, R> PendingRequests<K, R> {
175    /// Create a registry with the default 6s timeout.
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Set the default timeout applied to requests registered *after* this
181    /// call. Takes `&self`, so it works through a shared/cloned handle.
182    /// Already-registered requests keep the timeout they were created with.
183    pub fn set_timeout(&self, timeout: Duration) {
184        self.inner
185            .timeout_ms
186            .store(timeout.as_millis() as u64, Ordering::Relaxed);
187    }
188
189    /// The current default timeout.
190    pub fn timeout(&self) -> Duration {
191        Duration::from_millis(self.inner.timeout_ms.load(Ordering::Relaxed))
192    }
193
194    /// Number of requests currently awaiting a response.
195    pub fn len(&self) -> usize {
196        self.inner.requests.lock().len()
197    }
198
199    /// Whether there are no requests currently awaiting a response.
200    pub fn is_empty(&self) -> bool {
201        self.inner.requests.lock().is_empty()
202    }
203
204    /// Whether a request is currently pending for `key`.
205    pub fn contains(&self, key: &K) -> bool {
206        self.inner.requests.lock().contains_key(key)
207    }
208
209    /// Register a request under `key`, using the registry's default timeout.
210    ///
211    /// Returns [`Error::KeyAlreadyExists`] if a request with `key` is already
212    /// pending.
213    pub fn prepare_response(&self, key: K) -> Result<ResponseWaiter<K, R>> {
214        self.prepare_response_with_timeout(key, self.timeout())
215    }
216
217    /// Register a request under `key` with an explicit `timeout`.
218    pub fn prepare_response_with_timeout(
219        &self,
220        key: K,
221        timeout: Duration,
222    ) -> Result<ResponseWaiter<K, R>> {
223        let mut guard = self.inner.requests.lock();
224        if guard.contains_key(&key) {
225            return Err(Error::KeyAlreadyExists);
226        }
227
228        let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
229        let (tx, rx) = oneshot::channel();
230        guard.insert(key.clone(), Entry { id, sender: tx });
231
232        Ok(ResponseWaiter {
233            receiver: rx,
234            sleep: Box::pin(sleep(timeout)),
235            inner: Arc::downgrade(&self.inner),
236            key,
237            id,
238            done: false,
239        })
240    }
241
242    /// Deliver `response` to the request registered under `key`.
243    ///
244    /// Returns [`Error::KeyNotFound`] if no request is pending for `key`, or
245    /// [`Error::ReceiverDropped`] if the waiter was already dropped.
246    pub fn handle_response(&self, key: K, response: R) -> Result<()> {
247        let entry = self
248            .inner
249            .requests
250            .lock()
251            .remove(&key)
252            .ok_or(Error::KeyNotFound)?;
253
254        entry
255            .sender
256            .send(response)
257            .map_err(|_| Error::ReceiverDropped)
258    }
259
260    /// Cancel the request registered under `key`, causing its waiter to
261    /// resolve with [`Error::Canceled`].
262    ///
263    /// Returns [`Error::KeyNotFound`] if no request is pending for `key`.
264    pub fn cancel(&self, key: &K) -> Result<()> {
265        self.inner
266            .requests
267            .lock()
268            .remove(key)
269            .map(|_| ())
270            .ok_or(Error::KeyNotFound)
271    }
272
273    /// Cancel every pending request, resolving all their waiters with
274    /// [`Error::Canceled`]. Returns the number of requests canceled.
275    ///
276    /// Useful when a connection drops and all in-flight requests should fail
277    /// at once.
278    pub fn cancel_all(&self) -> usize {
279        let mut guard = self.inner.requests.lock();
280        let n = guard.len();
281        guard.clear();
282        n
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[tokio::test]
291    async fn roundtrip_delivers_response() {
292        let requests = PendingRequests::<u64, &str>::new();
293        let waiter = requests.prepare_response(1).unwrap();
294
295        requests.handle_response(1, "pong").unwrap();
296
297        assert_eq!(waiter.await.unwrap(), "pong");
298        assert!(requests.is_empty());
299    }
300
301    #[tokio::test]
302    async fn duplicate_key_is_rejected() {
303        let requests = PendingRequests::<u64, ()>::new();
304        let _waiter = requests.prepare_response(1).unwrap();
305
306        assert!(matches!(
307            requests.prepare_response(1),
308            Err(Error::KeyAlreadyExists)
309        ));
310    }
311
312    #[tokio::test]
313    async fn unknown_key_response_errors() {
314        let requests = PendingRequests::<u64, ()>::new();
315        assert!(matches!(
316            requests.handle_response(42, ()),
317            Err(Error::KeyNotFound)
318        ));
319    }
320
321    #[tokio::test(start_paused = true)]
322    async fn timeout_resolves_and_cleans_up() {
323        let requests = PendingRequests::<u64, ()>::new();
324        let waiter = requests
325            .prepare_response_with_timeout(1, Duration::from_millis(50))
326            .unwrap();
327
328        assert!(matches!(waiter.await, Err(Error::RequestTimeout)));
329        assert!(requests.is_empty(), "timed-out entry should be removed");
330    }
331
332    #[tokio::test]
333    async fn cancel_resolves_waiter() {
334        let requests = PendingRequests::<u64, ()>::new();
335        let waiter = requests.prepare_response(1).unwrap();
336
337        requests.cancel(&1).unwrap();
338
339        assert!(matches!(waiter.await, Err(Error::Canceled)));
340        assert!(matches!(requests.cancel(&1), Err(Error::KeyNotFound)));
341    }
342
343    #[tokio::test]
344    async fn dropped_waiter_is_reported_and_cleaned() {
345        let requests = PendingRequests::<u64, ()>::new();
346        let waiter = requests.prepare_response(1).unwrap();
347
348        drop(waiter);
349
350        assert!(
351            requests.is_empty(),
352            "dropped waiter should remove its entry"
353        );
354        assert!(matches!(
355            requests.handle_response(1, ()),
356            Err(Error::KeyNotFound)
357        ));
358    }
359
360    #[tokio::test]
361    async fn dropping_registry_cancels_waiter() {
362        let requests = PendingRequests::<u64, ()>::new();
363        let waiter = requests.prepare_response(1).unwrap();
364
365        drop(requests);
366
367        assert!(matches!(waiter.await, Err(Error::Canceled)));
368    }
369
370    #[tokio::test(start_paused = true)]
371    async fn reused_key_after_timeout_is_independent() {
372        let requests = PendingRequests::<u64, &str>::new();
373        let first = requests
374            .prepare_response_with_timeout(1, Duration::from_millis(50))
375            .unwrap();
376        assert!(matches!(first.await, Err(Error::RequestTimeout)));
377
378        // The same key can now be registered again and resolved normally.
379        let second = requests.prepare_response(1).unwrap();
380        requests.handle_response(1, "ok").unwrap();
381        assert_eq!(second.await.unwrap(), "ok");
382    }
383
384    #[tokio::test]
385    async fn clones_share_state() {
386        let requests = PendingRequests::<u64, &str>::new();
387        let other = requests.clone();
388
389        // Register on one handle, respond from the clone.
390        let waiter = requests.prepare_response(1).unwrap();
391        other.handle_response(1, "pong").unwrap();
392
393        assert_eq!(waiter.await.unwrap(), "pong");
394        assert!(other.is_empty());
395    }
396
397    #[tokio::test]
398    async fn contains_reflects_pending_state() {
399        let requests = PendingRequests::<u64, ()>::new();
400        assert!(!requests.contains(&1));
401
402        let _waiter = requests.prepare_response(1).unwrap();
403        assert!(requests.contains(&1));
404
405        requests.cancel(&1).unwrap();
406        assert!(!requests.contains(&1));
407    }
408
409    #[tokio::test]
410    async fn cancel_all_fails_every_waiter() {
411        let requests = PendingRequests::<u64, ()>::new();
412        let a = requests.prepare_response(1).unwrap();
413        let b = requests.prepare_response(2).unwrap();
414
415        assert_eq!(requests.cancel_all(), 2);
416        assert!(requests.is_empty());
417        assert!(matches!(a.await, Err(Error::Canceled)));
418        assert!(matches!(b.await, Err(Error::Canceled)));
419    }
420
421    #[tokio::test]
422    async fn set_timeout_works_through_shared_handle() {
423        let requests = PendingRequests::<u64, ()>::new();
424        let handle = requests.clone();
425
426        handle.set_timeout(Duration::from_secs(30));
427        assert_eq!(requests.timeout(), Duration::from_secs(30));
428    }
429
430    #[tokio::test]
431    async fn surviving_clone_keeps_waiter_alive() {
432        let requests = PendingRequests::<u64, &str>::new();
433        let clone = requests.clone();
434        let waiter = requests.prepare_response(1).unwrap();
435
436        // Dropping one handle must not cancel requests while a clone lives.
437        drop(requests);
438        clone.handle_response(1, "still here").unwrap();
439        assert_eq!(waiter.await.unwrap(), "still here");
440    }
441}