1mod builder;
4mod config;
5
6pub use builder::{PacketPipelineBuilder, Unset};
7use bytes::Bytes;
8pub use config::{AcknowledgementPipelineConfig, PacketPipelineConfig, PoolArbitrationConfig};
9use futures::{SinkExt, StreamExt, future::Either};
10use futures_time::{future::FutureExt as TimeExt, stream::StreamExt as TimeStreamExt};
11use hopr_api::{
12 PeerId,
13 node::TicketEvent,
14 types::{crypto::prelude::*, internal::prelude::*},
15};
16use hopr_crypto_packet::HoprSurb;
17use hopr_protocol_app::prelude::*;
18use hopr_protocol_hopr::prelude::*;
19use hopr_utils::{
20 network_types::timeout::{SinkTimeoutError, TimeoutSinkExt, TimeoutStreamExt},
21 runtime::AbortableList,
22};
23use tracing::Instrument;
24
25use crate::PeerProtocolCounterRegistry;
26
27const DEFAULT_ACK_INPUT_CONCURRENCY: usize = 10;
30const DEFAULT_ACK_OUTPUT_CONCURRENCY: usize = 10;
33const QUEUE_SEND_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(50);
34const PACKET_DECODING_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
47const PACKET_ENCODING_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
48
49const PIPELINE_CONCURRENCY_PER_CPU: usize = 8;
51
52fn default_pipeline_concurrency(available_parallelism: usize) -> usize {
62 available_parallelism.max(1) * PIPELINE_CONCURRENCY_PER_CPU
63}
64
65#[cfg(all(feature = "telemetry", not(test)))]
66lazy_static::lazy_static! {
67 static ref METRIC_PACKET_COUNT: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
68 "hopr_packets_count",
69 "Number of processed packets of different types (sent, received, forwarded)",
70 &["type"]
71 ).unwrap();
72 static ref METRIC_PACKET_REJECTED_COUNT: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
73 "hopr_packet_rejected_count",
74 "Number of incoming packets rejected due various reasons",
75 &["reason"]
76 ).unwrap();
77 static ref METRIC_PACKET_DECODE_TIMEOUTS: hopr_api::types::telemetry::SimpleCounter = hopr_api::types::telemetry::SimpleCounter::new(
82 "hopr_packet_decode_timeouts_total",
83 "Number of incoming packets dropped due to decode timeout (sustained rate indicates Rayon pool saturation)"
84 ).unwrap();
85 static ref METRIC_VALIDATION_ERRORS: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
86 "hopr_packet_ticket_validation_errors",
87 "Number of different ticket validation errors encountered during packet processing",
88 &["type"]
89 ).unwrap();
90 static ref METRIC_RECEIVED_ACKS: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
91 "hopr_protocol_ack_received_count",
92 "Number of received acknowledgements",
93 &["valid"]
94 ).unwrap();
95 static ref METRIC_SENT_ACKS: hopr_api::types::telemetry::SimpleCounter = hopr_api::types::telemetry::SimpleCounter::new(
96 "hopr_protocol_ack_sent_count",
97 "Number of sent message acknowledgements"
98 ).unwrap();
99 static ref METRIC_TICKETS_COUNT: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
100 "hopr_tickets_count",
101 "Number of tickets by type (winning, losing, rejected)",
102 &["type"]
103 ).unwrap();
104}
105
106#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, strum::Display)]
107pub enum PacketPipelineProcesses {
108 #[strum(to_string = "HOPR [msg] - ingress")]
109 MsgIn,
110 #[strum(to_string = "HOPR [msg] - egress")]
111 MsgOut,
112 #[strum(to_string = "HOPR [ack] - egress")]
113 AckOut,
114 #[strum(to_string = "HOPR [ack] - ingress")]
115 AckIn,
116 #[strum(to_string = "HOPR [msg] - mixer")]
117 Mixer,
118}
119
120async fn start_outgoing_packet_pipeline<AppOut, E, WOut, WOutErr>(
122 app_outgoing: AppOut,
123 encoder: std::sync::Arc<E>,
124 wire_outgoing: WOut,
125 counters: super::counters::PeerProtocolCounterRegistry,
126 concurrency: usize,
127) where
128 AppOut: futures::Stream<Item = (ResolvedTransportRouting<HoprSurb>, ApplicationDataOut)> + Send + 'static,
129 E: PacketEncoder + Send + Sync + 'static,
130 WOut: futures::Sink<(PeerId, Bytes), Error = SinkTimeoutError<WOutErr>> + Clone + Unpin + Send + 'static,
131 WOutErr: std::error::Error,
132{
133 let res = app_outgoing
134 .map(|(routing, data)| {
138 let encoder = encoder.clone();
139 let counters = counters.clone();
140 async move {
141 hopr_transport_session::counters::ENCODE_STAGE_ENTRIES
142 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
143 match hopr_utils::parallelize::cpu::spawn_encode_blocking(
144 move || {
145 encoder.encode_packet(
146 data.data.to_bytes(),
147 routing,
148 data.packet_info
149 .map(|data| data.signals_to_destination)
150 .unwrap_or_default(),
151 )
152 },
153 "packet_encode",
154 )
155 .timeout(futures_time::time::Duration::from(PACKET_ENCODING_TIMEOUT))
156 .await
157 {
158 Ok(Ok(Ok(packet))) => {
159 #[cfg(all(feature = "telemetry", not(test)))]
160 METRIC_PACKET_COUNT.increment(&["sent"]);
161
162 counters.get_or_create(&packet.next_hop).record_message_sent();
163 tracing::trace!(peer = packet.next_hop.to_peerid_str(), "protocol message out");
164 Some((packet.next_hop.into(), packet.data))
165 }
166 Ok(Ok(Err(error))) => {
167 tracing::error!(%error, "outgoing packet could not be encoded");
168 None
169 }
170 Ok(Err(error)) => {
171 tracing::error!(%error, "parallel processing of the outgoing packet failed");
172 None
173 }
174 Err(error) => {
175 tracing::error!(%error, "timeout while processing the outgoing packet");
176 hopr_utils::parallelize::cpu::ENCODE_TIMEOUT_DROPS
177 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
178 None
179 }
180 }
181 }
182 })
183 .buffered(concurrency)
184 .filter_map(futures::future::ready)
185 .map(Ok)
186 .forward_to_timeout(wire_outgoing)
187 .in_current_span()
188 .await;
189
190 if let Err(error) = res {
191 tracing::error!(
192 task = "transport (protocol - msg egress)",
193 %error,
194 "long-running background task finished with error"
195 );
196 } else {
197 tracing::warn!(
198 task = "transport (protocol - msg egress)",
199 "long-running background task finished"
200 )
201 }
202}
203
204#[allow(clippy::too_many_arguments)]
211async fn start_incoming_packet_pipeline<WIn, WOut, D, T, TEvt, AckIn, AckOut, AppIn, AppInErr>(
212 (wire_outgoing, wire_incoming): (WOut, WIn),
213 decoder: std::sync::Arc<D>,
214 ticket_proc: std::sync::Arc<T>,
215 ticket_events: TEvt,
216 (ack_outgoing, ack_incoming): (AckOut, AckIn),
217 app_incoming: AppIn,
218 counters: super::counters::PeerProtocolCounterRegistry,
219 concurrency: usize,
220) where
221 WIn: futures::Stream<Item = (PeerId, Bytes)> + Send + 'static,
222 WOut: futures::Sink<(PeerId, Bytes)> + Clone + Unpin + Send + 'static,
223 WOut::Error: std::error::Error,
224 D: PacketDecoder + Sync + Send + 'static,
225 T: UnacknowledgedTicketProcessor + Send + 'static,
226 TEvt: futures::Sink<TicketEvent> + Clone + Unpin + Send + 'static,
227 TEvt::Error: std::error::Error,
228 AckIn: futures::Sink<(OffchainPublicKey, Vec<Acknowledgement>)> + Send + Unpin + Clone + 'static,
229 AckIn::Error: std::error::Error,
230 AckOut: futures::Sink<(OffchainPublicKey, Option<HalfKey>)> + Send + Unpin + Clone + 'static,
231 AckOut::Error: std::error::Error,
232 AppIn: futures::Sink<(HoprPseudonym, ApplicationDataIn), Error = SinkTimeoutError<AppInErr>> + Send + 'static,
233 AppInErr: std::error::Error,
234{
235 let ack_outgoing_success = ack_outgoing.clone();
236 let ack_outgoing_failure = ack_outgoing;
237 let ticket_proc_success = ticket_proc;
238
239 let res = wire_incoming
240 .map(move |(peer, data)| {
245 let decoder = decoder.clone();
246 let mut ack_outgoing_failure = ack_outgoing_failure.clone();
247 let mut ticket_events_reject = ticket_events.clone();
248
249 tracing::trace!(%peer, "protocol message in");
250
251 async move {
252 match hopr_utils::parallelize::cpu::spawn_decode_blocking(move || decoder.decode(peer, data), "packet_decode")
253 .timeout(futures_time::time::Duration::from(PACKET_DECODING_TIMEOUT))
254 .await {
255 Ok(Ok(Ok(packet))) => {
256 tracing::trace!(%peer, ?packet, "successfully decoded incoming packet");
257 Some(packet)
258 },
259 Ok(Ok(Err(IncomingPacketError::Undecodable(error)))) => {
260 tracing::trace!(%peer, %error, "not sending ack back on undecodable packet - possible adversarial behavior");
264
265 #[cfg(all(feature = "telemetry", not(test)))]
266 METRIC_PACKET_REJECTED_COUNT.increment(&["undecodable"]);
267
268 None
269 },
270 Ok(Ok(Err(IncomingPacketError::ProcessingError(sender, error)))) => {
271 tracing::error!(%peer, %error, "failed to process the decoded packet");
272 ack_outgoing_failure
274 .send((*sender, None))
275 .await
276 .unwrap_or_else(|error| {
277 tracing::error!(%error, "failed to send ack to the egress queue");
278 });
279
280 #[cfg(all(feature = "telemetry", not(test)))]
281 METRIC_PACKET_REJECTED_COUNT.increment(&["processing_error"]);
282
283 None
284 },
285 Ok(Ok(Err(IncomingPacketError::InvalidTicket(sender, error)))) => {
286 tracing::error!(%peer, %error, "failed to validate ticket on the received packet");
287 if let Err(error) = ticket_events_reject
288 .send(TicketEvent::RejectedTicket(error.ticket, error.issuer))
289 .await {
290 tracing::error!(%error, "failed to notify invalid ticket rejection");
291 }
292 ack_outgoing_failure
294 .send((*sender, None))
295 .await
296 .unwrap_or_else(|error| {
297 tracing::error!(%error, "failed to send ack to the egress queue");
298 });
299
300 #[cfg(all(feature = "telemetry", not(test)))]
301 {
302 METRIC_VALIDATION_ERRORS.increment(&[error.kind.as_ref()]);
303 METRIC_PACKET_REJECTED_COUNT.increment(&["invalid_ticket"]);
304 METRIC_TICKETS_COUNT.increment(&["rejected"]);
305 }
306
307 None
308 }
309 Ok(Err(error)) => {
310 tracing::error!(%error, "parallel processing of the incoming packet failed");
311 None
312 },
313 Err(_) => {
314 tracing::error!(
316 %peer,
317 timeout_ms = PACKET_DECODING_TIMEOUT.as_millis() as u64,
318 "dropped incoming packet: decode timeout - check the 'hopr_rayon_queue_wait_seconds' metric for pool saturation"
319 );
320 hopr_utils::parallelize::cpu::DECODE_TIMEOUT_DROPS
321 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
322 #[cfg(all(feature = "telemetry", not(test)))]
323 {
324 METRIC_PACKET_DECODE_TIMEOUTS.increment();
325 METRIC_PACKET_REJECTED_COUNT.increment(&["timeout"]);
326 }
327
328 None
329 }
330 }
331 }.instrument(tracing::debug_span!("incoming_packet_decode", %peer))
332 })
333 .buffered(concurrency)
334 .filter_map(futures::future::ready)
335 .map(move |packet| {
340 match packet {
341 IncomingPacket::Acknowledgement(ack) => {
342 let mut ack_incoming = ack_incoming.clone();
343 let counters = counters.clone();
344 Either::Left(async move {
345 let IncomingAcknowledgementPacket { previous_hop, received_acks, .. } = *ack;
346 tracing::trace!(previous_hop = previous_hop.to_peerid_str(), num_acks = received_acks.len(), "incoming acknowledgements");
347 counters.get_or_create(&previous_hop).record_acks_received(received_acks.len() as u64);
348
349 ack_incoming
350 .send((previous_hop, received_acks))
351 .await
352 .unwrap_or_else(|error| {
353 tracing::error!(%error, "failed dispatching received acknowledgement to the ticket ack queue");
354 });
355
356 None
358 })
359 }
360 IncomingPacket::Final(final_packet) => {
361 let mut ack_outgoing_success = ack_outgoing_success.clone();
362 Either::Right(Either::Left(async move {
363 let IncomingFinalPacket {
364 previous_hop,
365 sender,
366 plain_text,
367 ack_key,
368 info,
369 ..
370 } = *final_packet;
371 tracing::trace!(previous_hop = previous_hop.to_peerid_str(), "incoming final packet");
372
373 ack_outgoing_success
375 .send((previous_hop, Some(ack_key)))
376 .await
377 .unwrap_or_else(|error| {
378 tracing::error!(%error, "failed to send ack to the egress queue");
379 });
380
381 #[cfg(all(feature = "telemetry", not(test)))]
382 METRIC_PACKET_COUNT.increment(&["received"]);
383
384 Some((sender, plain_text, info))
385 }))
386 }
387 IncomingPacket::Forwarded(fwd_packet) => {
388 let ticket_proc = ticket_proc_success.clone();
389 let mut wire_outgoing = wire_outgoing.clone();
390 let mut ack_outgoing_success = ack_outgoing_success.clone();
391 let counters = counters.clone();
392 Either::Right(Either::Right(async move {
393 let IncomingForwardedPacket {
394 previous_hop,
395 next_hop,
396 data,
397 ack_key_prev_hop,
398 ack_challenge,
399 received_ticket,
400 ..
401 } = *fwd_packet;
402 if let Err(error) = ticket_proc.insert_unacknowledged_ticket(&next_hop, ack_challenge, received_ticket) {
404 tracing::error!(
405 previous_hop = previous_hop.to_peerid_str(),
406 next_hop = next_hop.to_peerid_str(),
407 %error,
408 "failed to insert unacknowledged ticket into the ticket processor"
409 );
410
411 #[cfg(all(feature = "telemetry", not(test)))]
412 METRIC_PACKET_REJECTED_COUNT.increment(&["unack_processing_error"]);
413
414 return None;
415 }
416
417 tracing::trace!(
419 previous_hop = previous_hop.to_peerid_str(),
420 next_hop = next_hop.to_peerid_str(),
421 "forwarding packet to the next hop"
422 );
423
424 match wire_outgoing.send((next_hop.into(), data)).await {
425 Ok(()) => {
426 counters.get_or_create(&next_hop).record_message_sent();
427
428 #[cfg(all(feature = "telemetry", not(test)))]
429 METRIC_PACKET_COUNT.increment(&["forwarded"]);
430 }
431 Err(error) => {
432 tracing::error!(%error, "failed to forward a packet to the transport layer");
433 return None;
434 }
435 }
436
437 tracing::trace!(previous_hop = previous_hop.to_peerid_str(), "acknowledging forwarded packet back");
439 ack_outgoing_success
440 .send((previous_hop, Some(ack_key_prev_hop)))
441 .await
442 .unwrap_or_else(|error| {
443 tracing::error!(%error, "failed to send ack to the egress queue");
444 });
445
446 None
447 }))
448 }
449 }
450 })
451 .buffered(concurrency)
452 .filter_map(|maybe_data| futures::future::ready(
453 maybe_data
455 .and_then(|(sender, data, aux_info)| ApplicationData::try_from(data.as_ref())
456 .inspect_err(|error| tracing::error!(%sender, %error, "failed to decode application data"))
457 .ok()
458 .map(|data| (sender, ApplicationDataIn {
459 data,
460 packet_info: IncomingPacketInfo {
461 signals_from_sender: aux_info.packet_signals,
462 num_saved_surbs: aux_info.num_surbs,
463 num_evicted_surbs: aux_info.num_evicted_surbs,
464 }
465 })))
466 ))
467 .map(Ok)
468 .forward_to_timeout(app_incoming)
469 .in_current_span()
470 .await;
471
472 if let Err(error) = res {
473 tracing::error!(
474 task = "transport (protocol - msg ingress)",
475 %error,
476 "long-running background task finished with error"
477 );
478 } else {
479 tracing::warn!(
480 task = "transport (protocol - msg ingress)",
481 "long-running background task finished"
482 )
483 }
484}
485
486async fn start_outgoing_ack_pipeline<AckOut, E, WOut>(
487 ack_outgoing: AckOut,
488 encoder: std::sync::Arc<E>,
489 cfg: AcknowledgementPipelineConfig,
490 packet_key: OffchainKeypair,
491 wire_outgoing: WOut,
492) where
493 AckOut: futures::Stream<Item = (OffchainPublicKey, Option<HalfKey>)> + Send + 'static,
494 E: PacketEncoder + Sync + Send + 'static,
495 WOut: futures::Sink<(PeerId, Bytes)> + Clone + Unpin + Send + 'static,
496 WOut::Error: std::error::Error,
497{
498 ack_outgoing
499 .map(move |(destination, maybe_ack_key)| {
500 let packet_key = packet_key.clone();
501 let ack = maybe_ack_key
503 .map(|ack_key| VerifiedAcknowledgement::new(ack_key, &packet_key))
504 .unwrap_or_else(|| VerifiedAcknowledgement::random(&packet_key));
505 (destination, ack)
506 })
507 .buffer(futures_time::time::Duration::from(cfg.ack_buffer_interval))
508 .filter(|acks| futures::future::ready(!acks.is_empty()))
509 .scan(
517 halfbrown::HashMap::<OffchainPublicKey, Vec<VerifiedAcknowledgement>, ahash::RandomState>::with_capacity_and_hasher(
518 cfg.ack_grouping_capacity,
519 ahash::RandomState::default(),
520 ),
521 |groups, buffered_acks| {
522 for (dst, ack) in buffered_acks {
523 groups
524 .entry(dst)
525 .and_modify(|v| v.push(ack))
526 .or_insert_with(|| vec![ack]);
527 }
528 tracing::trace!(
529 num_groups = groups.len(),
530 num_acks = groups.values().map(|v| v.len()).sum::<usize>(),
531 "acknowledgements grouped"
532 );
533 let drained: Vec<_> = groups.drain().collect();
534 futures::future::ready(Some(futures::stream::iter(drained)))
535 },
536 )
537 .flatten()
538 .for_each_concurrent(
539 cfg.ack_output_concurrency.filter(|&n| n > 0).unwrap_or(DEFAULT_ACK_OUTPUT_CONCURRENCY),
540 move |(destination, acks)| {
541 let encoder = encoder.clone();
542 let mut wire_outgoing = wire_outgoing.clone();
543 async move {
544 let c = acks.chunks(MAX_ACKNOWLEDGEMENTS_BATCH_SIZE).map(|c| c.to_vec()).collect::<Vec<_>>();
547 for ack_chunk in c {
548 let encoder = encoder.clone();
549 #[cfg(all(feature = "telemetry", not(test)))]
550 let ack_chunk_len = ack_chunk.len() as u64;
551 match hopr_utils::parallelize::cpu::spawn_fifo_blocking(move || encoder.encode_acknowledgements(&ack_chunk, &destination), "ack_encode").await {
552 Ok(Ok(ack_packet)) => {
553 wire_outgoing
554 .feed((ack_packet.next_hop.into(), ack_packet.data))
555 .await
556 .unwrap_or_else(|error| {
557 tracing::error!(%error, "failed to forward an acknowledgement to the transport layer");
558 });
559
560 #[cfg(all(feature = "telemetry", not(test)))]
561 METRIC_SENT_ACKS.increment_by(ack_chunk_len);
562 }
563 Ok(Err(error)) => tracing::error!(%error, "failed to encode acknowledgements"),
564 Err(error) => tracing::error!(%error, "parallel processing of the outgoing acknowledgements failed"),
565 }
566 }
567 if let Err(error) = wire_outgoing.flush().await {
568 tracing::error!(%error, "failed to flush acknowledgements batch to the transport layer");
569 }
570 tracing::trace!("acknowledgements out");
571 }.instrument(tracing::debug_span!("outgoing_ack_batch", peer = destination.to_peerid_str()))
572 }
573 )
574 .in_current_span()
575 .await;
576
577 tracing::warn!(
578 task = "transport (protocol - ack outgoing)",
579 "long-running background task finished"
580 );
581}
582
583async fn start_drain_incoming_ack_pipeline<AckIn>(ack_incoming: AckIn)
591where
592 AckIn: futures::Stream<Item = (OffchainPublicKey, Vec<Acknowledgement>)> + Send + 'static,
593{
594 ack_incoming
595 .for_each(move |(peer, acks)| {
596 tracing::trace!(%peer, num = acks.len(), "received acknowledgements (drained, not processed)");
597 futures::future::ready(())
598 })
599 .in_current_span()
600 .await;
601
602 tracing::warn!(
603 task = "transport (protocol - ticket acknowledgement drain)",
604 "long-running background task finished"
605 );
606}
607
608async fn start_relay_incoming_ack_pipeline<AckIn, T, TEvt>(
609 ack_incoming: AckIn,
610 ticket_events: TEvt,
611 ticket_proc: std::sync::Arc<T>,
612 concurrency: usize,
613) where
614 AckIn: futures::Stream<Item = (OffchainPublicKey, Vec<Acknowledgement>)> + Send + 'static,
615 T: UnacknowledgedTicketProcessor + Sync + Send + 'static,
616 TEvt: futures::Sink<TicketEvent> + Clone + Unpin + Send + 'static,
617 TEvt::Error: std::error::Error,
618{
619 ack_incoming
620 .for_each_concurrent(concurrency, move |(peer, acks)| {
621 let ticket_proc = ticket_proc.clone();
622 let mut ticket_evt = ticket_events.clone();
623 async move {
624 tracing::trace!(num = acks.len(), "received acknowledgements");
625 match hopr_utils::parallelize::cpu::spawn_fifo_blocking(
626 move || ticket_proc.acknowledge_tickets(peer, acks),
627 "ack_decode",
628 )
629 .await
630 {
631 Ok(Ok(resolutions)) if !resolutions.is_empty() => {
632 let resolutions_iter = resolutions.into_iter().filter_map(|resolution| match resolution {
633 ResolvedAcknowledgement::RelayingWin(redeemable_ticket) => {
634 tracing::trace!("received ack for a winning ticket");
635 #[cfg(all(feature = "telemetry", not(test)))]
636 {
637 METRIC_RECEIVED_ACKS.increment(&["true"]);
638 METRIC_TICKETS_COUNT.increment(&["winning"]);
639 }
640 Some(Ok(TicketEvent::WinningTicket(redeemable_ticket)))
641 }
642 ResolvedAcknowledgement::RelayingLoss(_) => {
643 tracing::trace!("received ack for a losing ticket");
645 #[cfg(all(feature = "telemetry", not(test)))]
646 {
647 METRIC_RECEIVED_ACKS.increment(&["true"]);
648 METRIC_TICKETS_COUNT.increment(&["losing"]);
649 }
650 None
651 }
652 });
653
654 if let Err(error) = ticket_evt.send_all(&mut futures::stream::iter(resolutions_iter)).await {
656 tracing::error!(%error, "failed to notify ticket resolutions");
657 }
658 }
659 Ok(Ok(_)) => {
660 tracing::debug!("acknowledgement batch could not acknowledge any ticket");
661 }
662 Ok(Err(TicketAcknowledgementError::UnexpectedAcknowledgement)) => {
663 tracing::trace!("received unexpected acknowledgement");
666 }
667 Ok(Err(error)) => {
668 tracing::error!(%error, "failed to acknowledge ticket");
669 }
670 Err(error) => {
671 tracing::error!(%error, "parallel processing of the incoming acknowledgements failed")
672 }
673 }
674 }
675 .instrument(tracing::debug_span!("incoming_ack_batch", peer = peer.to_peerid_str()))
676 })
677 .in_current_span()
678 .await;
679
680 tracing::warn!(
681 task = "transport (protocol - ticket acknowledgement)",
682 "long-running background task finished"
683 );
684}
685#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
694pub enum NodeType {
695 Relay,
696 Entry,
697 Exit,
698}
699
700#[derive(Debug, Default, Copy, Clone)]
705#[doc(hidden)]
706pub struct NoopTicketProcessor;
707
708impl UnacknowledgedTicketProcessor for NoopTicketProcessor {
709 type Error = std::convert::Infallible;
710
711 #[inline]
712 fn insert_unacknowledged_ticket(
713 &self,
714 _: &OffchainPublicKey,
715 _: HalfKeyChallenge,
716 _: UnacknowledgedTicket,
717 ) -> Result<(), Self::Error> {
718 Ok(())
719 }
720
721 #[inline]
722 fn acknowledge_tickets(
723 &self,
724 _: OffchainPublicKey,
725 _: Vec<Acknowledgement>,
726 ) -> Result<Vec<ResolvedAcknowledgement>, TicketAcknowledgementError<Self::Error>> {
727 Ok(Vec::with_capacity(0))
728 }
729}
730#[allow(clippy::too_many_arguments)]
733#[tracing::instrument(skip_all, level = "trace", fields(me = packet_key.public().to_peerid_str()))]
734pub(super) fn run_packet_pipeline_inner<WIn, WOut, C, D, T, TEvt, AppOut, AppIn>(
735 node_type: NodeType,
736 packet_key: OffchainKeypair,
737 wire_msg: (WOut, WIn),
738 codec: (C, D),
739 ticket_proc: T,
740 ticket_events: TEvt,
741 cfg: PacketPipelineConfig,
742 api: (AppOut, AppIn),
743 counters: PeerProtocolCounterRegistry,
744) -> AbortableList<PacketPipelineProcesses>
745where
746 WOut: futures::Sink<(PeerId, Bytes)> + Clone + Unpin + Send + 'static,
747 WOut::Error: std::error::Error,
748 WIn: futures::Stream<Item = (PeerId, Bytes)> + Send + 'static,
749 C: PacketEncoder + Sync + Send + 'static,
750 D: PacketDecoder + Sync + Send + 'static,
751 T: UnacknowledgedTicketProcessor + Sync + Send + 'static,
752 TEvt: futures::Sink<TicketEvent> + Clone + Unpin + Send + 'static,
753 TEvt::Error: std::error::Error,
754 AppOut: futures::Sink<(HoprPseudonym, ApplicationDataIn)> + Send + 'static,
755 AppOut::Error: std::error::Error,
756 AppIn: futures::Stream<Item = (ResolvedTransportRouting<HoprSurb>, ApplicationDataOut)> + Send + 'static,
757{
758 let mut processes = AbortableList::default();
759
760 #[cfg(all(feature = "telemetry", not(test)))]
761 {
762 lazy_static::initialize(&METRIC_PACKET_COUNT);
764 lazy_static::initialize(&METRIC_PACKET_DECODE_TIMEOUTS);
765 lazy_static::initialize(&METRIC_PACKET_REJECTED_COUNT);
766 lazy_static::initialize(&METRIC_VALIDATION_ERRORS);
767 }
768
769 let (outgoing_ack_tx, outgoing_ack_rx) = hopr_utils::network_types::crossfire_sink::bounded_sink_channel::<(
770 OffchainPublicKey,
771 Option<HalfKey>,
772 )>(cfg.ack_config.ack_out_buffer_size);
773
774 let (incoming_ack_tx, incoming_ack_rx) = hopr_utils::network_types::crossfire_sink::bounded_sink_channel::<(
775 OffchainPublicKey,
776 Vec<Acknowledgement>,
777 )>(cfg.ack_config.ticket_ack_buffer_size);
778
779 let (wire_out, wire_in) = (wire_msg.0.with_timeout(QUEUE_SEND_TIMEOUT), wire_msg.1);
782 let (app_out, app_in) = (api.0.with_timeout(QUEUE_SEND_TIMEOUT), api.1);
783 let incoming_ack_tx = incoming_ack_tx.with_timeout(QUEUE_SEND_TIMEOUT);
784 let outgoing_ack_tx = outgoing_ack_tx.with_timeout(QUEUE_SEND_TIMEOUT);
785 let ticket_events = ticket_events.with_timeout(QUEUE_SEND_TIMEOUT);
786
787 let encoder = std::sync::Arc::new(codec.0);
788 let decoder = std::sync::Arc::new(codec.1);
789 let ticket_proc = std::sync::Arc::new(ticket_proc);
790
791 let available_parallelism = std::thread::available_parallelism().ok().map(|n| n.get()).unwrap_or(1);
795 let default_concurrency = default_pipeline_concurrency(available_parallelism);
796
797 let output_concurrency = cfg.output_concurrency.filter(|&n| n > 0).unwrap_or(default_concurrency);
798 let input_concurrency = cfg.input_concurrency.filter(|&n| n > 0).unwrap_or(default_concurrency);
799
800 hopr_utils::parallelize::cpu::with_arbitration_once(cfg.arbitration.to_arbitration());
806
807 processes.insert(
808 PacketPipelineProcesses::MsgOut,
809 hopr_utils::spawn_as_abortable!(
810 start_outgoing_packet_pipeline(
811 app_in,
812 encoder.clone(),
813 wire_out.clone(),
814 counters.clone(),
815 output_concurrency
816 )
817 .in_current_span()
818 ),
819 );
820
821 processes.insert(
822 PacketPipelineProcesses::MsgIn,
823 hopr_utils::spawn_as_abortable!(
824 start_incoming_packet_pipeline(
825 (wire_out.clone(), wire_in),
826 decoder,
827 ticket_proc.clone(),
828 ticket_events.clone(),
829 (outgoing_ack_tx, incoming_ack_tx),
830 app_out,
831 counters.clone(),
832 input_concurrency,
833 )
834 .in_current_span()
835 ),
836 );
837
838 processes.insert(
839 PacketPipelineProcesses::AckOut,
840 hopr_utils::spawn_as_abortable!(
841 start_outgoing_ack_pipeline(outgoing_ack_rx, encoder, cfg.ack_config, packet_key.clone(), wire_out,)
842 .in_current_span()
843 ),
844 );
845
846 let ack_input_concurrency = cfg
847 .ack_config
848 .ack_input_concurrency
849 .filter(|&n| n > 0)
850 .unwrap_or(DEFAULT_ACK_INPUT_CONCURRENCY);
851
852 match node_type {
853 NodeType::Relay => {
854 processes.insert(
855 PacketPipelineProcesses::AckIn,
856 hopr_utils::spawn_as_abortable!(
857 start_relay_incoming_ack_pipeline(
858 incoming_ack_rx,
859 ticket_events,
860 ticket_proc,
861 ack_input_concurrency
862 )
863 .in_current_span()
864 ),
865 );
866 }
867 NodeType::Exit => {
868 let _ = (ticket_events, ticket_proc, ack_input_concurrency);
872 processes.insert(
873 PacketPipelineProcesses::AckIn,
874 hopr_utils::spawn_as_abortable!(start_drain_incoming_ack_pipeline(incoming_ack_rx).in_current_span()),
875 );
876 }
877 NodeType::Entry => {
878 let _ = (ticket_events, ticket_proc, ack_input_concurrency);
882 processes.insert(
883 PacketPipelineProcesses::AckIn,
884 hopr_utils::spawn_as_abortable!(start_drain_incoming_ack_pipeline(incoming_ack_rx).in_current_span()),
885 );
886 }
887 }
888
889 processes
890}
891
892#[cfg(test)]
893mod tests {
894 use futures::channel::mpsc;
895
896 use super::*;
897
898 #[test]
905 fn default_pipeline_concurrency_is_cpu_scaled() {
906 assert_eq!(default_pipeline_concurrency(4), 4 * PIPELINE_CONCURRENCY_PER_CPU);
907 for cpus in 1..=16 {
908 assert!(default_pipeline_concurrency(cpus) >= cpus);
909 }
910 assert!(default_pipeline_concurrency(0) >= 1);
911 }
912
913 #[tokio::test]
923 async fn drain_pipeline_keeps_receiver_alive() {
924 let (tx, rx) = mpsc::channel::<(OffchainPublicKey, Vec<Acknowledgement>)>(16);
925 let drain = tokio::spawn(start_drain_incoming_ack_pipeline(rx));
926
927 tokio::task::yield_now().await;
929
930 assert!(!tx.is_closed(), "drain task must hold the receiver alive");
932
933 drop(tx);
935 drain
936 .await
937 .expect("drain task must finish cleanly after sender is dropped");
938 }
939
940 #[tokio::test]
942 async fn drain_pipeline_completes_on_empty_stream() {
943 let (tx, rx) = mpsc::channel::<(OffchainPublicKey, Vec<Acknowledgement>)>(32);
944 let drain = tokio::spawn(start_drain_incoming_ack_pipeline(rx));
945 drop(tx);
946 drain
947 .await
948 .expect("drain task must finish cleanly after sender is dropped");
949 }
950
951 #[tokio::test]
953 async fn disconnected_sender_returns_err_not_panic() {
954 let (tx, rx) = mpsc::channel::<u8>(4);
955 drop(rx);
956 let mut tx2 = tx.clone();
957 let result = tx2.send(42u8).await;
958 assert!(result.is_err(), "send to disconnected receiver must return Err");
959 }
960
961 #[tokio::test]
964 async fn session_management_dispatcher_survives_disconnected_sink() -> anyhow::Result<()> {
965 use anyhow::Context;
966 use futures::SinkExt;
967
968 let (data_tx, data_rx) = mpsc::channel::<u8>(4);
969
970 drop(data_rx);
972
973 let upstream = futures::stream::iter(0u8..10);
975
976 let dispatcher = tokio::spawn(async move {
978 upstream
979 .for_each(move |item| {
980 let mut tx = data_tx.clone();
981 async move {
982 let _ = tx.send(item).await;
984 }
985 })
986 .await;
987 });
988
989 dispatcher
991 .await
992 .context("dispatcher must complete cleanly even with a disconnected sink")?;
993 Ok(())
994 }
995
996 #[tokio::test]
999 async fn session_management_dispatcher_does_not_drop_upstream_on_disconnected_sink() -> anyhow::Result<()> {
1000 use anyhow::Context;
1001 use futures::SinkExt;
1002
1003 let (data_tx, data_rx) = mpsc::channel::<u8>(1);
1004 drop(data_rx); let (upstream_tx, upstream_rx) = mpsc::channel::<u8>(16);
1007 let upstream = upstream_rx;
1008
1009 let dispatcher = tokio::spawn(async move {
1010 upstream
1011 .for_each(move |item| {
1012 let mut tx = data_tx.clone();
1013 async move {
1014 let _ = tx.send(item).await;
1015 }
1016 })
1017 .await;
1018 });
1019
1020 let mut sender = upstream_tx;
1022 for i in 0u8..20 {
1023 sender.send(i).await.context("upstream send must succeed")?;
1024 }
1025 drop(sender); dispatcher
1028 .await
1029 .context("dispatcher must complete when upstream closes, even with disconnected sink")?;
1030 Ok(())
1031 }
1032
1033 #[tokio::test]
1050 async fn buffered_decode_preserves_arrival_order_under_inverted_latency() {
1051 use std::time::Duration;
1052
1053 const N: usize = 16;
1054 let concurrency = N; let output: Vec<usize> = futures::stream::iter(0..N)
1057 .map(|i| async move {
1058 tokio::time::sleep(Duration::from_millis(((N - i) * 4) as u64)).await;
1060 if i == 3 { None } else { Some(i) }
1062 })
1063 .buffered(concurrency)
1064 .filter_map(futures::future::ready)
1065 .collect()
1066 .await;
1067
1068 let expected: Vec<usize> = (0..N).filter(|&i| i != 3).collect();
1069 assert_eq!(
1070 output, expected,
1071 "buffered(concurrency) must emit decoded packets in arrival order regardless of decode latency; got \
1072 {output:?}. A regression to then_concurrent()/FuturesUnordered would emit completion order and fail here."
1073 );
1074 }
1075}