Skip to main content

commonware_resolver/
delivery.rs

1//! Track pending consumer deliveries for resolver implementations.
2//!
3//! Resolvers often need the same delivery lifecycle: keep a fetch alive while
4//! `Consumer::deliver` validates a response, abort that validation if the fetch
5//! is pruned, and reuse a cached response for subscribers that were added
6//! while validation was in progress. This module owns that lifecycle without
7//! making assumptions about how data is fetched.
8
9use crate::{Consumer, Delivery, Outcome};
10use commonware_utils::futures::{AbortablePool, Aborter};
11use futures::future::Aborted;
12use std::collections::{HashMap, hash_map::Entry as HashMapEntry};
13use tracing::debug;
14
15/// Completed consumer validation for a delivery.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct Completion<K, S, Context = ()> {
18    /// Resolver-specific context associated with the delivery attempt.
19    pub context: Context,
20
21    /// Key and subscribers that were passed to the consumer.
22    pub delivery: Delivery<K, S>,
23
24    /// Consumer disposition for the delivered response, or `None` if the
25    /// consumer dropped the sender without reporting one.
26    pub outcome: Option<Outcome>,
27}
28
29// Cached response that can be redelivered while it is accepted or still unjudged.
30struct Response<Context, V> {
31    context: Context,
32    value: V,
33    accepted: bool,
34}
35
36// Active validation attempt for a key.
37struct ActiveDelivery {
38    generation: u64,
39    _aborter: Aborter,
40}
41
42// Pooled validation result tagged with the attempt that produced it.
43struct PooledCompletion<K, S, Context> {
44    generation: u64,
45    completion: Completion<K, S, Context>,
46}
47
48// Per-key delivery state retained while a resolver fetch is active.
49struct Entry<Context, V, State> {
50    delivery: Option<ActiveDelivery>,
51    response: Option<Response<Context, V>>,
52    state: Option<State>,
53}
54
55impl<Context, V, State> Entry<Context, V, State> {
56    const fn new(state: State) -> Self {
57        Self {
58            delivery: None,
59            response: None,
60            state: Some(state),
61        }
62    }
63}
64
65/// Tracks in-flight consumer deliveries keyed by resolver key.
66///
67/// `Context` carries resolver-specific metadata back to the caller when
68/// validation completes.
69///
70/// `State` holds per-key resolver state that should be dropped when the key is
71/// pruned, or explicitly taken when the resolver completes the fetch.
72pub struct Tracker<Con, Context = (), State = ()>
73where
74    Con: Consumer,
75    Con::Value: Clone + Send + 'static,
76    Context: Clone + Send + 'static,
77{
78    entries: HashMap<Con::Key, Entry<Context, Con::Value, State>>,
79    deliveries: AbortablePool<'static, PooledCompletion<Con::Key, Con::Subscriber, Context>>,
80    next_generation: u64,
81    consumer: Con,
82}
83
84impl<Con, Context, State> Tracker<Con, Context, State>
85where
86    Con: Consumer,
87    Con::Value: Clone + Send + 'static,
88    Context: Clone + Send + 'static,
89{
90    /// Create an empty tracker backed by the provided consumer.
91    pub fn new(consumer: Con) -> Self {
92        Self {
93            entries: HashMap::new(),
94            deliveries: AbortablePool::default(),
95            next_generation: 0,
96            consumer,
97        }
98    }
99
100    /// Returns true when the key has an active tracked fetch.
101    pub fn contains(&self, key: &Con::Key) -> bool {
102        self.entries.contains_key(key)
103    }
104
105    /// Start tracking a key.
106    ///
107    /// Returns `true` when the key was inserted. If the key is already tracked,
108    /// this leaves the existing entry untouched and returns `false`.
109    pub(crate) fn insert_with_state(&mut self, key: Con::Key, state: State) -> bool {
110        match self.entries.entry(key) {
111            HashMapEntry::Vacant(entry) => {
112                entry.insert(Entry::new(state));
113                true
114            }
115            HashMapEntry::Occupied(_) => false,
116        }
117    }
118
119    /// Remove a key and abort any in-progress delivery for it.
120    ///
121    /// Returns true if the key was present. Any completion produced by an
122    /// aborted delivery is discarded by [`next_completion`](Self::next_completion).
123    pub fn remove(&mut self, key: &Con::Key) -> bool {
124        self.entries.remove(key).is_some()
125    }
126
127    /// Remove a key, aborting any in-progress delivery, and return its state.
128    ///
129    /// Returns `None` if the key was absent. The inner `Option` is `None` if the
130    /// state was already taken while the key stayed active.
131    pub(crate) fn remove_with_state(&mut self, key: &Con::Key) -> Option<Option<State>> {
132        self.entries.remove(key).map(|entry| entry.state)
133    }
134
135    /// Take the key's state without removing the tracked key.
136    ///
137    /// Returns `None` when the key is absent or the state was already taken.
138    pub(crate) fn take_state(&mut self, key: &Con::Key) -> Option<State> {
139        self.entries
140            .get_mut(key)
141            .and_then(|entry| entry.state.take())
142    }
143
144    /// Retain only entries for which the predicate returns true.
145    ///
146    /// Dropped entries abort in-progress deliveries. Returns the number of
147    /// removed entries.
148    pub fn retain<F: FnMut(&Con::Key) -> bool>(&mut self, mut predicate: F) -> usize {
149        self.entries.extract_if(|key, _| !predicate(key)).count()
150    }
151
152    /// Remove all entries and abort all in-progress deliveries.
153    ///
154    /// Returns the number of entries removed.
155    pub fn drain(&mut self) -> usize {
156        let count = self.entries.len();
157        self.entries.clear();
158        count
159    }
160
161    /// Deliver a newly received response to the consumer.
162    ///
163    /// The response is cached so that later retained subscribers can be
164    /// redelivered the same bytes with [`redeliver`](Self::redeliver) once the
165    /// consumer accepts it or drops its verdict. Panics if the key is not tracked.
166    pub fn deliver(
167        &mut self,
168        delivery: Delivery<Con::Key, Con::Subscriber>,
169        context: Context,
170        value: Con::Value,
171    ) {
172        let key = delivery.key.clone();
173        let entry = self.entries.get_mut(&key).expect("delivery entry");
174        entry.response = Some(Response {
175            context: context.clone(),
176            value: value.clone(),
177            accepted: false,
178        });
179        self.push_delivery(delivery, context, value);
180    }
181
182    /// Deliver the cached response to another set of subscribers.
183    ///
184    /// This is intended for subscribers added while an earlier validation was
185    /// still pending. The cached response is either accepted or still unjudged
186    /// because the consumer dropped the earlier verdict. Panics if the key is not
187    /// tracked or no response is cached.
188    pub fn redeliver(&mut self, delivery: Delivery<Con::Key, Con::Subscriber>) {
189        let key = delivery.key.clone();
190        let (context, value) = {
191            let entry = self.entries.get(&key).expect("delivery entry");
192            let response = entry.response.as_ref().expect("response");
193            (response.context.clone(), response.value.clone())
194        };
195        self.push_delivery(delivery, context, value);
196    }
197
198    /// Returns true if the cached response for this key has been accepted.
199    pub fn response_accepted(&self, key: &Con::Key) -> bool {
200        self.entries
201            .get(key)
202            .and_then(|entry| entry.response.as_ref())
203            .is_some_and(|response| response.accepted)
204    }
205
206    /// Mark the cached response accepted by the consumer.
207    ///
208    /// Panics if the key is not tracked or no response is cached.
209    pub fn accept_response(&mut self, key: &Con::Key) {
210        let entry = self.entries.get_mut(key).expect("delivery entry");
211        let response = entry.response.as_mut().expect("response");
212        response.accepted = true;
213    }
214
215    /// Drop the cached response without removing the tracked key.
216    ///
217    /// Use this when a response is invalid or does not satisfy every delivered
218    /// subscriber and the resolver wants to retry the key.
219    pub fn discard_response(&mut self, key: &Con::Key) {
220        if let Some(entry) = self.entries.get_mut(key) {
221            entry.response = None;
222        }
223    }
224
225    /// Wait for the next consumer validation result.
226    ///
227    /// Returns [`Aborted`] when the delivery was canceled before completion. A
228    /// verdict sender dropped by the consumer completes with no outcome.
229    /// Successful completions clear the active delivery slot for that key so it
230    /// can be retried or redelivered. Completions for an older same-key delivery
231    /// are treated as aborted.
232    pub async fn next_completion(
233        &mut self,
234    ) -> Result<Completion<Con::Key, Con::Subscriber, Context>, Aborted> {
235        let completed = self.deliveries.next_completed().await?;
236        let Some(entry) = self.entries.get_mut(&completed.completion.delivery.key) else {
237            return Err(Aborted);
238        };
239        if entry
240            .delivery
241            .as_ref()
242            .is_none_or(|delivery| delivery.generation != completed.generation)
243        {
244            return Err(Aborted);
245        }
246        entry.delivery = None;
247        Ok(completed.completion)
248    }
249
250    // Start a consumer validation attempt and record its abort handle.
251    fn push_delivery(
252        &mut self,
253        delivery: Delivery<Con::Key, Con::Subscriber>,
254        context: Context,
255        value: Con::Value,
256    ) {
257        let generation = self.next_generation;
258        self.next_generation = self
259            .next_generation
260            .checked_add(1)
261            .expect("delivery generation overflow");
262        let key = delivery.key.clone();
263        let completed = delivery.clone();
264        let mut consumer = self.consumer.clone();
265        let receiver = consumer.deliver(delivery, value);
266        let aborter = self.deliveries.push(async move {
267            let outcome = match receiver.await {
268                Ok(outcome) => Some(outcome.into()),
269                Err(_) => {
270                    debug!(key = ?completed.key, "consumer dropped delivery without a verdict");
271                    None
272                }
273            };
274            PooledCompletion {
275                generation,
276                completion: Completion {
277                    context,
278                    delivery: completed,
279                    outcome,
280                },
281            }
282        });
283        let entry = self.entries.get_mut(&key).expect("delivery entry");
284        assert!(
285            entry
286                .delivery
287                .replace(ActiveDelivery {
288                    generation,
289                    _aborter: aborter,
290                })
291                .is_none()
292        );
293    }
294}
295
296impl<Con, Context> Tracker<Con, Context>
297where
298    Con: Consumer,
299    Con::Value: Clone + Send + 'static,
300    Context: Clone + Send + 'static,
301{
302    /// Start tracking a key without any resolver-specific state.
303    ///
304    /// Returns `true` when the key was inserted. If the key is already tracked,
305    /// this leaves the existing entry untouched and returns `false`.
306    pub fn insert(&mut self, key: Con::Key) -> bool {
307        self.insert_with_state(key, ())
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::p2p::mocks::{Consumer as MockConsumer, Key as MockKey};
315    use bytes::Bytes;
316    use commonware_runtime::{Runner as _, deterministic::Runner};
317    use commonware_utils::{
318        channel::{fallible::FallibleExt, mpsc, oneshot},
319        non_empty_vec,
320    };
321
322    type TestTracker = Tracker<MockConsumer<MockKey, Bytes>, u8>;
323
324    fn delivery(key: MockKey) -> Delivery<MockKey, ()> {
325        Delivery {
326            key,
327            subscribers: non_empty_vec![((), tracing::Span::none())],
328        }
329    }
330
331    #[derive(Clone)]
332    struct PendingConsumer {
333        sender: mpsc::UnboundedSender<oneshot::Sender<bool>>,
334    }
335
336    impl PendingConsumer {
337        fn new() -> (Self, mpsc::UnboundedReceiver<oneshot::Sender<bool>>) {
338            let (sender, receiver) = mpsc::unbounded_channel();
339            (Self { sender }, receiver)
340        }
341    }
342
343    impl Consumer for PendingConsumer {
344        type Key = MockKey;
345        type Value = Bytes;
346        type Subscriber = ();
347        type Outcome = bool;
348
349        fn deliver(
350            &mut self,
351            _delivery: Delivery<Self::Key, Self::Subscriber>,
352            _value: Self::Value,
353        ) -> oneshot::Receiver<bool> {
354            let (sender, receiver) = oneshot::channel();
355            self.sender.send_lossy(sender);
356            receiver
357        }
358    }
359
360    #[test]
361    fn test_insert_contains_remove_round_trip() {
362        let runner = Runner::default();
363        runner.start(|_| async move {
364            let mut tracker = TestTracker::new(MockConsumer::dummy());
365
366            assert!(!tracker.contains(&MockKey(1)));
367            assert!(tracker.insert(MockKey(1)));
368            assert!(tracker.contains(&MockKey(1)));
369
370            assert!(!tracker.insert(MockKey(1)));
371            assert!(tracker.remove(&MockKey(1)));
372            assert!(!tracker.contains(&MockKey(1)));
373            assert!(!tracker.remove(&MockKey(1)));
374        });
375    }
376
377    #[test]
378    fn test_deliver_completes_with_context_and_consumer_result() {
379        let runner = Runner::default();
380        runner.start(|_| async move {
381            let (consumer, mut events) = MockConsumer::<MockKey, Bytes>::new();
382            let mut tracker = TestTracker::new(consumer);
383            let key = MockKey(7);
384            let value = Bytes::from("data");
385
386            tracker.insert(key.clone());
387            tracker.deliver(delivery(key.clone()), 9, value.clone());
388
389            let completed = tracker
390                .next_completion()
391                .await
392                .expect("delivery should complete");
393            assert_eq!(completed.context, 9);
394            assert_eq!(completed.delivery.key, key);
395            assert_eq!(completed.outcome, Some(Outcome::Complete));
396
397            let (delivered_key, delivered_value) = events.recv().await.unwrap();
398            assert_eq!(delivered_key, key);
399            assert_eq!(delivered_value, value);
400        });
401    }
402
403    #[test]
404    fn test_remove_aborts_in_flight_delivery() {
405        let runner = Runner::default();
406        runner.start(|_| async move {
407            let (consumer, _events) = MockConsumer::<MockKey, Bytes>::new();
408            let mut tracker = TestTracker::new(consumer);
409            let key = MockKey(1);
410
411            tracker.insert(key.clone());
412            tracker.deliver(delivery(key.clone()), 2, Bytes::from("v"));
413            assert!(tracker.remove(&key));
414
415            assert!(matches!(tracker.next_completion().await, Err(Aborted)));
416        });
417    }
418
419    #[test]
420    fn test_stale_same_key_completion_does_not_clear_new_delivery() {
421        let runner = Runner::default();
422        runner.start(|_| async move {
423            let (consumer, mut senders) = PendingConsumer::new();
424            let mut tracker = Tracker::<PendingConsumer, u8>::new(consumer);
425            let key = MockKey(1);
426
427            tracker.insert(key.clone());
428            tracker.deliver(delivery(key.clone()), 1, Bytes::from("old"));
429            let old_sender = senders.recv().await.unwrap();
430            old_sender.send(true).unwrap();
431            let stale = tracker.deliveries.next_completed().await.unwrap();
432
433            assert!(tracker.remove(&key));
434            tracker.insert(key.clone());
435            tracker.deliver(delivery(key.clone()), 2, Bytes::from("new"));
436            let new_sender = senders.recv().await.unwrap();
437
438            let _stale_aborter = tracker.deliveries.push(async move { stale });
439            assert!(matches!(tracker.next_completion().await, Err(Aborted)));
440
441            new_sender.send(true).unwrap();
442            let completed = tracker
443                .next_completion()
444                .await
445                .expect("new delivery should complete");
446            assert_eq!(completed.context, 2);
447            assert_eq!(completed.delivery.key, key);
448            assert_eq!(completed.outcome, Some(Outcome::Complete));
449        });
450    }
451
452    #[test]
453    fn test_dropped_verdict_completes_without_outcome_and_redelivers() {
454        let runner = Runner::default();
455        runner.start(|_| async move {
456            let (consumer, mut senders) = PendingConsumer::new();
457            let mut tracker = Tracker::<PendingConsumer, u8>::new(consumer);
458            let key = MockKey(3);
459
460            tracker.insert(key.clone());
461            tracker.deliver(delivery(key.clone()), 4, Bytes::from("unjudged"));
462            drop(senders.recv().await.unwrap());
463
464            let completed = tracker
465                .next_completion()
466                .await
467                .expect("dropped verdict should complete");
468            assert_eq!(completed.context, 4);
469            assert_eq!(completed.delivery.key, key);
470            assert_eq!(completed.outcome, None);
471            assert!(!tracker.response_accepted(&key));
472
473            // The unjudged response can still be handed to other subscribers.
474            tracker.redeliver(delivery(key.clone()));
475            senders.recv().await.unwrap().send(true).unwrap();
476            let judged = tracker
477                .next_completion()
478                .await
479                .expect("redelivery should complete");
480            assert_eq!(judged.context, 4);
481            assert_eq!(judged.outcome, Some(Outcome::Complete));
482        });
483    }
484
485    #[test]
486    fn test_redeliver_reuses_accepted_response_for_new_subscribers() {
487        let runner = Runner::default();
488        runner.start(|_| async move {
489            let (consumer, mut events) = MockConsumer::<MockKey, Bytes>::new();
490            let mut tracker = TestTracker::new(consumer);
491            let key = MockKey(5);
492            let value = Bytes::from("first");
493
494            tracker.insert(key.clone());
495            tracker.deliver(delivery(key.clone()), 3, value.clone());
496
497            let completed = tracker
498                .next_completion()
499                .await
500                .expect("first delivery should complete");
501            assert_eq!(completed.outcome, Some(Outcome::Complete));
502            tracker.accept_response(&key);
503            assert!(tracker.response_accepted(&key));
504
505            tracker.redeliver(delivery(key.clone()));
506            let redelivered = tracker
507                .next_completion()
508                .await
509                .expect("redelivery should complete");
510            assert_eq!(redelivered.context, 3);
511            assert_eq!(redelivered.delivery.key, key);
512            assert_eq!(redelivered.outcome, Some(Outcome::Complete));
513
514            let first = events.recv().await.unwrap();
515            let second = events.recv().await.unwrap();
516            assert_eq!(first, (key.clone(), value.clone()));
517            assert_eq!(second, (key, value));
518        });
519    }
520
521    #[test]
522    #[should_panic(expected = "response")]
523    fn test_redeliver_requires_cached_response() {
524        let runner = Runner::default();
525        runner.start(|_| async move {
526            let (consumer, _events) = MockConsumer::<MockKey, Bytes>::new();
527            let mut tracker = TestTracker::new(consumer);
528            let key = MockKey(7);
529
530            tracker.insert(key.clone());
531            tracker.deliver(delivery(key.clone()), 3, Bytes::from("first"));
532            let completed = tracker
533                .next_completion()
534                .await
535                .expect("first delivery should complete");
536            assert_eq!(completed.outcome, Some(Outcome::Complete));
537
538            tracker.discard_response(&key);
539            tracker.redeliver(delivery(key));
540        });
541    }
542
543    #[test]
544    fn test_rejected_response_can_be_discarded_and_replaced() {
545        let runner = Runner::default();
546        runner.start(|_| async move {
547            let (mut consumer, _events) = MockConsumer::<MockKey, Bytes>::new();
548            let key = MockKey(8);
549            consumer.add_expected(key.clone(), Bytes::from("good"));
550            let mut tracker = TestTracker::new(consumer);
551
552            tracker.insert(key.clone());
553            tracker.deliver(delivery(key.clone()), 1, Bytes::from("bad"));
554            let rejected = tracker
555                .next_completion()
556                .await
557                .expect("rejected delivery should complete");
558            assert_eq!(rejected.outcome, Some(Outcome::Invalid));
559
560            tracker.discard_response(&key);
561            assert!(!tracker.response_accepted(&key));
562            tracker.deliver(delivery(key.clone()), 2, Bytes::from("good"));
563
564            let accepted = tracker
565                .next_completion()
566                .await
567                .expect("accepted delivery should complete");
568            assert_eq!(accepted.context, 2);
569            assert_eq!(accepted.outcome, Some(Outcome::Complete));
570        });
571    }
572}