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