Skip to main content

commonware_resolver/p2p/
engine.rs

1use super::{
2    Producer,
3    config::Config,
4    fetcher::{Config as FetcherConfig, Fetcher},
5    inflight::Inflight,
6    ingress::{FetchKey, Mailbox, Message},
7    metrics, wire,
8};
9use crate::{Consumer, Delivery, Outcome, Resolution, delivery::Completion, subscribers};
10use bytes::Bytes;
11use commonware_actor::mailbox;
12use commonware_cryptography::PublicKey;
13use commonware_macros::select_loop;
14use commonware_p2p::{
15    Blocker, Provider, Receiver, Recipients, Sender,
16    utils::codec::{WrappedSender, wrap},
17};
18use commonware_runtime::{
19    Clock, ContextCell, Handle, Metrics, Spawner, spawn_cell,
20    telemetry::metrics::{GaugeExt, histogram, status::Status},
21};
22use commonware_utils::{Span, channel::oneshot, futures::Pool as FuturesPool};
23use futures::future::{self, Either};
24use rand_core::Rng;
25use tracing::{debug, error, trace, warn};
26
27/// Represents a pending serve operation.
28struct Serve<P: PublicKey> {
29    timer: histogram::Timer,
30    peer: P,
31    id: u64,
32    result: Result<Bytes, oneshot::error::RecvError>,
33}
34
35/// Manages incoming and outgoing P2P requests, coordinating fetch and serve operations.
36pub struct Engine<E, P, D, B, Key, Con, Pro>
37where
38    E: Clock + Spawner + Rng + Metrics,
39    P: PublicKey,
40    D: Provider<PublicKey = P>,
41    B: Blocker<PublicKey = P>,
42    Key: Span,
43    Con: Consumer<Key = Key, Value = Bytes>,
44    Pro: Producer<Key = Key>,
45{
46    /// Context used to spawn tasks, manage time, etc.
47    context: ContextCell<E>,
48
49    /// Produces data for incoming requests
50    producer: Pro,
51
52    /// Manages the list of peers that can be used to fetch data
53    peer_provider: D,
54
55    /// The blocker that will be used to block peers that send invalid responses
56    blocker: B,
57
58    /// Used to detect changes in the peer set
59    last_peer_set_id: Option<u64>,
60
61    /// Mailbox that makes and prunes fetches
62    mailbox: mailbox::Receiver<Message<Key, P, Con::Subscriber>>,
63
64    /// Manages outgoing fetch requests
65    fetcher: Fetcher<E, P, Key>,
66
67    /// Tracks all in-flight fetch state
68    inflight: Inflight<Con, P>,
69
70    /// Subscribers that keep each fetch alive.
71    subscribers: subscribers::Tracker<Key, Con::Subscriber>,
72
73    /// Holds futures that resolve once the `Producer` has produced the data.
74    /// Once the future is resolved, the data (or an error) is sent to the peer.
75    /// Has unbounded size; the number of concurrent requests should be limited
76    /// by the `Producer` which may drop requests.
77    serves: FuturesPool<'static, Serve<P>>,
78
79    /// Whether responses are sent with priority over other network messages
80    priority_responses: bool,
81
82    /// Metrics for the peer actor
83    metrics: metrics::Metrics,
84}
85
86impl<E, P, D, B, Key, Con, Pro> Engine<E, P, D, B, Key, Con, Pro>
87where
88    E: Clock + Spawner + Rng + Metrics,
89    P: PublicKey,
90    D: Provider<PublicKey = P>,
91    B: Blocker<PublicKey = P>,
92    Key: Span,
93    Con: Consumer<Key = Key, Value = Bytes>,
94    Pro: Producer<Key = Key>,
95    Con::Subscriber: Ord,
96{
97    /// Creates a new `Actor` with the given configuration.
98    ///
99    /// Returns the actor and a mailbox to send messages to it.
100    pub fn new(
101        context: E,
102        cfg: Config<P, D, B, Key, Con, Pro>,
103    ) -> (Self, Mailbox<Key, P, Con::Subscriber>) {
104        let (sender, receiver) = mailbox::new(context.child("mailbox"), cfg.mailbox_size);
105
106        let metrics = metrics::Metrics::init(&context);
107        let fetcher = Fetcher::new(
108            context.child("fetcher"),
109            FetcherConfig {
110                me: cfg.me,
111                fetch_timeout: cfg.fetch_timeout,
112                retry_delay: cfg.fetch_retry_delay,
113                priority_requests: cfg.priority_requests,
114            },
115        );
116        (
117            Self {
118                context: ContextCell::new(context),
119                producer: cfg.producer,
120                peer_provider: cfg.peer_provider,
121                blocker: cfg.blocker,
122                last_peer_set_id: None,
123                mailbox: receiver,
124                fetcher,
125                inflight: Inflight::new(cfg.consumer),
126                subscribers: subscribers::Tracker::new(),
127                serves: FuturesPool::default(),
128                priority_responses: cfg.priority_responses,
129                metrics,
130            },
131            Mailbox::new(sender),
132        )
133    }
134
135    /// Runs the actor until the context is stopped.
136    ///
137    /// The actor will handle:
138    /// - Fetching data from other peers and notifying the `Consumer`
139    /// - Serving data to other peers by requesting it from the `Producer`
140    pub fn start<NetS: Sender<PublicKey = P>, NetR: Receiver<PublicKey = P>>(
141        mut self,
142        network: (NetS, NetR),
143    ) -> Handle<()> {
144        spawn_cell!(self.context, self.run(network))
145    }
146
147    /// Inner run loop called by `start`.
148    async fn run<NetS: Sender<PublicKey = P>, NetR: Receiver<PublicKey = P>>(
149        mut self,
150        network: (NetS, NetR),
151    ) {
152        // Wrap channel
153        let (mut sender, mut receiver) = wrap((), network.0, network.1);
154        let mut peer_set_subscription = self.peer_provider.subscribe().await;
155        let mut blocked_subscription = Some(self.blocker.blocked());
156
157        select_loop! {
158            self.context,
159            on_start => {
160                // Wait for the next blocked-set update, or forever once the
161                // network stops publishing them.
162                let blocked_update = blocked_subscription.as_mut().map_or_else(
163                    || Either::Right(future::pending()),
164                    |subscription| Either::Left(subscription.recv()),
165                );
166
167                // Update metrics
168                let _ = self
169                    .metrics
170                    .fetch_pending
171                    .try_set(self.fetcher.len_pending());
172                let _ = self.metrics.fetch_active.try_set(self.fetcher.len_active());
173                let _ = self.metrics.serve_processing.try_set(self.serves.len());
174
175                // Get retry deadline (if any)
176                let deadline_pending = match self.fetcher.get_pending_deadline() {
177                    Some(deadline) => Either::Left(self.context.sleep_until(deadline)),
178                    None => Either::Right(future::pending()),
179                };
180
181                // Get requester timeout (if any)
182                let deadline_active = match self.fetcher.get_active_deadline() {
183                    Some(deadline) => Either::Left(self.context.sleep_until(deadline)),
184                    None => Either::Right(future::pending()),
185                };
186            },
187            on_stopped => {
188                debug!("shutdown");
189                self.inflight.drain();
190                self.subscribers.clear();
191                self.serves.cancel_all();
192            },
193            // Handle peer set updates
194            Some(update) = peer_set_subscription.recv() else {
195                debug!("peer set subscription closed");
196                return;
197            } => {
198                if self.last_peer_set_id < Some(update.index) {
199                    self.last_peer_set_id = Some(update.index);
200                    self.fetcher.reconcile(update.latest.union().as_ref());
201                }
202            },
203            // Handle blocked-set updates
204            blocked = blocked_update => {
205                match blocked {
206                    Some(blocked) => self.fetcher.set_blocked(blocked),
207                    None => {
208                        debug!("blocked subscription closed");
209                        blocked_subscription = None;
210                    }
211                }
212            },
213            // Handle active deadline
214            _ = deadline_active => {
215                if let Some(key) = self.fetcher.pop_active() {
216                    debug!(?key, "requester timeout");
217                    self.metrics.fetch.inc(Status::Failure);
218                    self.fetcher.add_retry(key);
219                }
220            },
221            // Poll delivery completions before mailbox work so a ready completion
222            // can retire its subscribers before a queued fetch is coalesced with them.
223            delivery = self.inflight.next_delivery() => {
224                if let Ok(completed) = delivery {
225                    self.handle_delivery(completed);
226                }
227            },
228            // Handle mailbox messages
229            Some(msg) = self.mailbox.recv() else {
230                error!("mailbox closed");
231                return;
232            } => {
233                match msg {
234                    Message::Fetch(keys) => {
235                        for FetchKey {
236                            key,
237                            subscribers,
238                            metadata: targets,
239                        } in keys
240                        {
241                            trace!(?key, "mailbox: fetch");
242
243                            // Check if the fetch is already in progress
244                            let is_new = !self.inflight.contains(&key);
245                            self.subscribers.insert(key.clone(), subscribers);
246
247                            // Update targets
248                            match targets {
249                                Some(targets) => {
250                                    // Only add targets if this is a new fetch OR the existing
251                                    // fetch already has targets. Don't restrict an "all" fetch
252                                    // (no targets) to specific targets.
253                                    if is_new || self.fetcher.has_targets(&key) {
254                                        self.fetcher.add_targets(key.clone(), targets);
255                                    }
256                                }
257                                None => self.fetcher.clear_targets(&key),
258                            }
259
260                            // Only start new fetch if not already in progress
261                            if is_new {
262                                self.inflight.insert(
263                                    key.clone(),
264                                    self.metrics.fetch_duration.timer(self.context.as_ref()),
265                                );
266                                self.fetcher.add_ready(key);
267                            } else {
268                                trace!(?key, "updated targets for existing fetch");
269                            }
270                        }
271                    }
272                    Message::Retain { predicate } => {
273                        trace!("mailbox: retain");
274
275                        self.subscribers
276                            .retain(|key, subscriber| predicate(key, subscriber));
277                        let subscribers = &self.subscribers;
278                        self.fetcher.retain(|key| subscribers.contains(key));
279                        let count = self.inflight.retain(|key| subscribers.contains(key)) as u64;
280                        self.record_cancellations(count);
281                    }
282                }
283            },
284            // Wake the loop when pending work becomes ready. The send is
285            // performed in `on_end` after the selected event is handled.
286            _ = deadline_pending => {},
287            // Handle completed server requests
288            serve = self.serves.next_completed() => {
289                let Serve {
290                    timer,
291                    peer,
292                    id,
293                    result,
294                } = serve;
295
296                // Metrics and logs
297                match result {
298                    Ok(_) => {
299                        timer.observe(self.context.as_ref());
300                        self.metrics.serve.inc(Status::Success);
301                    }
302                    Err(ref err) => {
303                        debug!(?err, ?peer, ?id, "serve failed");
304                        self.metrics.serve.inc(Status::Failure);
305                    }
306                }
307
308                // Send response to peer
309                self.handle_serve(&mut sender, peer, id, result, self.priority_responses);
310            },
311            // Handle network messages
312            msg = receiver.recv() => {
313                // Break if the receiver is closed
314                let (peer, msg) = match msg {
315                    Ok(msg) => msg,
316                    Err(err) => {
317                        error!(?err, "receiver closed");
318                        return;
319                    }
320                };
321
322                match msg {
323                    Ok(msg) => match msg.payload {
324                        wire::Payload::Request(key) => {
325                            self.handle_network_request(peer, msg.id, key)
326                        }
327                        wire::Payload::Response(response) => {
328                            self.handle_network_response(peer, msg.id, response)
329                        }
330                        wire::Payload::Error => self.handle_network_error_response(peer, msg.id),
331                    },
332                    Err(err) => {
333                        trace!(?err, ?peer, "decode failed");
334                    }
335                };
336            },
337            on_end => {
338                // Attempt at most one due outbound request after each selected
339                // event so sustained event traffic cannot starve pending work.
340                if self
341                    .fetcher
342                    .get_pending_deadline()
343                    .is_some_and(|deadline| deadline <= self.context.current())
344                {
345                    self.fetcher.fetch(&mut sender);
346                }
347            },
348        }
349    }
350
351    /// Record cancellation metrics for a retain-style operation.
352    fn record_cancellations(&mut self, count: u64) {
353        if count == 0 {
354            self.metrics.cancel.inc(Status::Dropped);
355        } else {
356            self.metrics.cancel.inc_by(Status::Success, count);
357        }
358    }
359
360    /// Handles the case where the application responds to a request from an external peer.
361    fn handle_serve<NetS: Sender<PublicKey = P>>(
362        &mut self,
363        sender: &mut WrappedSender<NetS, wire::Message<Key>>,
364        peer: P,
365        id: u64,
366        response: Result<Bytes, oneshot::error::RecvError>,
367        priority: bool,
368    ) {
369        // Encode message
370        let payload: wire::Payload<Key> = response.map_or_else(
371            |_| wire::Payload::Error,
372            |data| wire::Payload::Response(data),
373        );
374        let msg = wire::Message { id, payload };
375
376        // Send message to peer
377        let result = sender.send(Recipients::One(peer.clone()), msg, priority);
378
379        // Log result, but do not handle errors.
380        if result.is_empty() {
381            warn!(?peer, ?id, "serve send failed");
382        } else {
383            trace!(?peer, ?id, "serve sent");
384        };
385    }
386
387    /// Handle a network request from a peer.
388    fn handle_network_request(&mut self, peer: P, id: u64, key: Key) {
389        // Serve the request
390        trace!(?peer, ?id, "peer request");
391        let mut producer = self.producer.clone();
392        let timer = self.metrics.serve_duration.timer(self.context.as_ref());
393        let receiver = producer.produce(key);
394        self.serves.push(async move {
395            let result = receiver.await;
396            Serve {
397                timer,
398                peer,
399                id,
400                result,
401            }
402        });
403    }
404
405    /// Handle a network response from a peer.
406    fn handle_network_response(&mut self, peer: P, id: u64, response: Bytes) {
407        trace!(?peer, ?id, "peer response: data");
408
409        // Get the key associated with the response, if any
410        let Some((key, elapsed)) = self.fetcher.pop_response(id, &peer) else {
411            // It's possible that the key does not exist if the request was pruned.
412            return;
413        };
414
415        let Some(subscribers) = self.subscribers.pending(&key) else {
416            warn!(?key, "response for fetch with no subscribers");
417            self.inflight.cancel(&key);
418            return;
419        };
420        let delivery = Delivery { key, subscribers };
421
422        // The peer had the data, so deliver it to the consumer without blocking the engine.
423        self.inflight.deliver(delivery, peer, elapsed, response);
424    }
425
426    /// Handle completed delivery to the consumer.
427    fn handle_delivery(
428        &mut self,
429        completed: Completion<Key, Con::Subscriber, (P, std::time::Duration, usize)>,
430    ) {
431        let Completion {
432            context: (peer, elapsed, bytes),
433            delivery:
434                Delivery {
435                    key,
436                    subscribers: delivered,
437                },
438            outcome,
439            canceled,
440            previously_accepted: already_accepted,
441        } = completed;
442
443        // Source feedback belongs to the delivered response even if its fetch was
444        // canceled. Local redelivery of an accepted response cannot earn more credit.
445        match outcome {
446            Some(Outcome::Resolved(_)) if !already_accepted => {
447                self.fetcher.record_response(&peer, elapsed, bytes);
448                self.metrics.fetch.inc(Status::Success);
449            }
450            Some(Outcome::Unresolved) if !already_accepted => {
451                self.metrics.fetch.inc(Status::Ambiguous);
452            }
453            Some(Outcome::Invalid) => {
454                // Conflicting verdicts on an accepted response reflect local consumer
455                // state. Only the first validation can identify invalid peer data.
456                if already_accepted {
457                    warn!(
458                        ?key,
459                        "previously accepted response was rejected during local redelivery"
460                    );
461                } else {
462                    commonware_p2p::block!(self.blocker, peer, "invalid data received");
463                }
464                self.metrics.fetch.inc(Status::Failure);
465            }
466            _ => {}
467        }
468
469        // Canceled feedback cannot retire, retry, or redeliver a replacement fetch.
470        if canceled {
471            return;
472        }
473
474        // A dropped verdict says nothing about the response, only that the consumer
475        // did not judge it for these subscribers. Hand the response to the
476        // remaining subscribers, or retire the key when none remain.
477        let Some(outcome) = outcome else {
478            let remaining = self
479                .subscribers
480                .remove_delivered(&key, delivered.map_into(|(subscriber, _)| subscriber));
481            if let Some(subscribers) = remaining {
482                self.inflight.redeliver(Delivery { key, subscribers });
483                return;
484            }
485            if !already_accepted {
486                self.metrics.fetch.inc(Status::Dropped);
487            }
488            self.inflight.cancel(&key);
489            self.fetcher.clear(&key);
490            return;
491        };
492
493        // Resolution reports demand within the delivered subscriber snapshot.
494        // Ignored cancels demand for the entire key, including later subscribers.
495        match outcome {
496            Outcome::Resolved(Resolution::Complete) => {
497                // Remove only the subscribers that accepted this response. If other
498                // subscribers still need the key, deliver the same accepted response
499                // locally with the remaining annotations.
500                let remaining = self
501                    .subscribers
502                    .remove_delivered(&key, delivered.map_into(|(subscriber, _)| subscriber));
503
504                if let Some(subscribers) = remaining {
505                    if !already_accepted {
506                        self.inflight.accept_response(&key, self.context.as_ref());
507                    }
508                    self.inflight.redeliver(Delivery { key, subscribers });
509                } else {
510                    // All subscribers observed a valid response, so drop the key's targets
511                    // and peer-selection history.
512                    self.inflight.complete(self.context.as_ref(), &key);
513                    self.fetcher.clear(&key);
514                }
515            }
516            Outcome::Resolved(Resolution::Partial) | Outcome::Unresolved => {
517                // Valid data can leave subscribers unresolved. Discard it and retry
518                // without penalizing the peer or recounting an accepted response.
519                self.inflight.discard_response(&key);
520                self.fetcher.add_retry(key);
521            }
522            Outcome::Invalid => {
523                // An accepted response remains valid peer data. Retire conflicting
524                // local demand without fetching the same response again.
525                if already_accepted {
526                    self.inflight.complete(self.context.as_ref(), &key);
527                    self.subscribers.remove(&key);
528                    self.fetcher.clear(&key);
529                    return;
530                }
531
532                // Subscribers still need valid data, so discard the rejected response
533                // and schedule another attempt.
534                self.inflight.discard_response(&key);
535                self.fetcher.add_retry(key);
536            }
537            Outcome::Ignored => {
538                // The consumer no longer needs the key. Retire the entire fetch without
539                // scoring or blocking the response's source.
540                self.metrics.fetch.inc(Status::Dropped);
541                self.inflight.cancel(&key);
542                self.subscribers.remove(&key);
543                self.fetcher.clear(&key);
544            }
545        }
546    }
547
548    /// Handle a network response from a peer that did not have the data.
549    fn handle_network_error_response(&mut self, peer: P, id: u64) {
550        trace!(?peer, ?id, "peer response: error");
551
552        // Get the key associated with the response, if any
553        let Some(key) = self.fetcher.pop_missing(id, &peer) else {
554            // It's possible that the key does not exist if the request was pruned.
555            return;
556        };
557
558        // The peer did not have the data, so we need to try again
559        self.metrics.fetch.inc(Status::Failure);
560        self.fetcher.add_retry(key);
561    }
562}