Skip to main content

hopr_transport_session/
manager.rs

1use std::{
2    pin::Pin,
3    sync::{Arc, OnceLock, atomic::Ordering},
4    time::Duration,
5};
6
7use anyhow::anyhow;
8use futures::{Sink, SinkExt, StreamExt, TryStreamExt, future::AbortHandle};
9use futures_time::future::FutureExt as TimeExt;
10use hopr_api::types::{
11    crypto_random::Randomizable,
12    internal::{
13        prelude::HoprPseudonym,
14        routing::{DestinationRouting, RoutingOptions},
15    },
16    primitive::prelude::Address,
17};
18use hopr_crypto_packet::prelude::HoprPacket;
19use hopr_protocol_app::prelude::*;
20use hopr_protocol_start::{
21    KeepAliveFlag, KeepAliveMessage, StartChallenge, StartErrorReason, StartErrorType, StartEstablished,
22    StartInitiation,
23};
24use hopr_utils::runtime::AbortableList;
25use tracing::{debug, error, info, trace, warn};
26
27#[cfg(feature = "telemetry")]
28use crate::telemetry::{
29    SessionLifecycleState, initialize_session_metrics, remove_session_metrics_state, set_session_balancer_data,
30    set_session_state,
31};
32use crate::{
33    Capability, HoprSession, IncomingSession, SESSION_MTU, SessionClientConfig, SessionId, SessionTarget,
34    SurbBalancerConfig,
35    balancer::{
36        AtomicSurbFlowEstimator, BalancerStateValues, RateController, RateLimitSinkExt, SurbBalancer,
37        SurbControllerWithCorrection,
38        pid::{PidBalancerController, PidControllerGains},
39        simple::SimpleBalancerController,
40    },
41    errors::{SessionManagerError, TransportSessionError},
42    types::{ByteCapabilities, ClosureReason, HoprSessionConfig, HoprStartProtocol, SESSION_APPLICATION_TAG},
43    utils,
44    utils::{SurbNotificationMode, insert_into_next_slot},
45};
46
47#[cfg(all(feature = "telemetry", not(test)))]
48lazy_static::lazy_static! {
49    static ref METRIC_ACTIVE_SESSIONS: hopr_api::types::telemetry::SimpleGauge = hopr_api::types::telemetry::SimpleGauge::new(
50        "hopr_session_num_active_sessions",
51        "Number of currently active HOPR sessions"
52    ).unwrap();
53    static ref METRIC_NUM_ESTABLISHED_SESSIONS: hopr_api::types::telemetry::SimpleCounter = hopr_api::types::telemetry::SimpleCounter::new(
54        "hopr_session_established_sessions_count",
55        "Number of sessions that were successfully established as an Exit node"
56    ).unwrap();
57    static ref METRIC_NUM_INITIATED_SESSIONS: hopr_api::types::telemetry::SimpleCounter = hopr_api::types::telemetry::SimpleCounter::new(
58        "hopr_session_initiated_sessions_count",
59        "Number of sessions that were successfully initiated as an Entry node"
60    ).unwrap();
61    static ref METRIC_RECEIVED_SESSION_ERRS: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
62        "hopr_session_received_error_count",
63        "Number of HOPR session errors received from an Exit node",
64        &["kind"]
65    ).unwrap();
66    static ref METRIC_DISPATCHED_MSGS: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
67        "hopr_session_dispatched_messages",
68        "Number dispatched HOPR session messages and their classification",
69        &["kind"]
70    ).unwrap();
71    static ref METRIC_SENT_SESSION_ERRS: hopr_api::types::telemetry::MultiCounter = hopr_api::types::telemetry::MultiCounter::new(
72        "hopr_session_sent_error_count",
73        "Number of HOPR session errors sent to an Entry node",
74        &["kind"]
75    ).unwrap();
76}
77
78#[tracing::instrument(level = "debug", skip(session_data))]
79fn close_session(session_id: SessionId, session_data: SessionSlot, reason: ClosureReason) {
80    debug!("closing session");
81
82    #[cfg(feature = "telemetry")]
83    {
84        set_session_state(&session_id, SessionLifecycleState::Closed);
85        remove_session_metrics_state(&session_id);
86    }
87
88    if reason != ClosureReason::EmptyRead {
89        // Closing the data sender will also cause it to close from the read side
90        debug!("data tx channel closed on session");
91    }
92
93    // Terminate any additional tasks spawned by the Session
94    session_data.abort_handles.lock().abort_all();
95
96    #[cfg(all(feature = "telemetry", not(test)))]
97    METRIC_ACTIVE_SESSIONS.decrement(1.0);
98}
99
100fn initiation_timeout_max_one_way(base: Duration, hops: usize) -> Duration {
101    base * (hops as u32)
102}
103
104/// Minimum time the SURB buffer must endure if no SURBs are being produced.
105pub const MIN_SURB_BUFFER_DURATION: Duration = Duration::from_secs(1);
106/// Minimum time between SURB buffer notifications to the Entry.
107pub const MIN_SURB_BUFFER_NOTIFICATION_PERIOD: Duration = Duration::from_secs(1);
108
109/// The first challenge value used in Start protocol to initiate a session.
110pub(crate) const MIN_CHALLENGE: StartChallenge = 1;
111
112/// Maximum time to wait for counterparty to receive the target number of SURBs.
113const SESSION_READINESS_TIMEOUT: Duration = Duration::from_secs(10);
114
115/// Minimum timeout until an unfinished frame is discarded.
116const MIN_FRAME_TIMEOUT: Duration = Duration::from_millis(10);
117
118/// Timeout when sending Start protocol messages to the sink
119const EXTERNAL_SEND_TIMEOUT: Duration = Duration::from_millis(200);
120
121/// How many packets can be buffered if the HoprSession socket is not fast enough.
122#[allow(dead_code)]
123pub const SESSION_FORWARD_CAPACITY: usize = 10000;
124
125// Needs to use an UnboundedSender instead of oneshot
126// because Moka cache requires the value to be Clone, which oneshot Sender is not.
127// It also cannot be enclosed in an Arc, since calling `send` consumes the oneshot Sender.
128type SessionInitiationCache = moka::sync::Cache<
129    StartChallenge,
130    crossfire::MTx<crossfire::mpsc::One<Result<StartEstablished<SessionId>, StartErrorType>>>,
131>;
132
133/// Handles to streams and tasks spawned by the Session.
134#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, strum::Display)]
135enum SessionHandles {
136    /// Handle to the stream that facilitates ingress of data from the HOPR network into the Session.
137    Ingress,
138    /// Handle to the process that sends keep-alive messages to the Session recipient (Exit).
139    KeepAlive,
140    /// Handle to the process that monitors and balances SURBs.
141    Balancer,
142}
143
144#[derive(Clone)]
145pub(crate) struct SessionSlot {
146    // Sender does not need to be in Arc, because the receiver part is always
147    // wrapped inside DropAbortable wrapper, with abort handle added to `abort_handles`.
148    session_tx: crossfire::MTx<crossfire::mpsc::Array<ApplicationDataIn>>,
149    routing_opts: DestinationRouting,
150    // Additional tasks spawned by the Session.
151    abort_handles: Arc<parking_lot::Mutex<AbortableList<SessionHandles>>>,
152    // Allows reconfiguring of the SURB balancer on-the-fly
153    // Set on both Entry and Exit sides.
154    surb_mgmt: Arc<BalancerStateValues>,
155    // SURB flow updates happening outside of Session protocol
156    // (e.g., due to Start protocol messages).
157    surb_estimator: AtomicSurbFlowEstimator,
158}
159
160/// RAII guard that rolls back a freshly inserted [`SessionSlot`] unless the
161/// session setup is explicitly [committed](SessionSlotGuard::commit).
162///
163/// Establishing a session involves several fallible steps *after* the slot has
164/// been inserted into the Session cache (constructing the [`HoprSession`],
165/// notifying about the new session, sending the establishment message, ...).
166/// If any of these steps fails, the already inserted slot would otherwise linger
167/// in the cache until idle eviction, blocking the pseudonym (and counting towards
168/// `maximum_sessions`) in the meantime.
169///
170/// Dropping this guard without committing removes the slot and tears down the
171/// partially initialized session. Since Moka's removal is asynchronous and Rust
172/// has no asynchronous `Drop`, the cleanup is performed on a spawned task.
173struct SessionSlotGuard<'a> {
174    sessions: &'a moka::sync::Cache<SessionId, SessionSlot>,
175    active_sessions: Arc<std::sync::atomic::AtomicUsize>,
176    session_id: SessionId,
177    committed: bool,
178}
179
180impl<'a> SessionSlotGuard<'a> {
181    fn new(
182        sessions: &'a moka::sync::Cache<SessionId, SessionSlot>,
183        session_id: SessionId,
184        active_sessions: Arc<std::sync::atomic::AtomicUsize>,
185    ) -> Self {
186        Self {
187            sessions,
188            active_sessions,
189            session_id,
190            committed: false,
191        }
192    }
193
194    /// Marks the session as successfully established, preventing the slot from
195    /// being rolled back when this guard is dropped.
196    fn commit(&mut self) {
197        self.committed = true;
198
199        #[cfg(all(feature = "telemetry", not(test)))]
200        METRIC_ACTIVE_SESSIONS.increment(1.0);
201    }
202}
203
204impl Drop for SessionSlotGuard<'_> {
205    fn drop(&mut self) {
206        if !self.committed {
207            // The session setup failed after the slot was inserted: remove it so it does
208            // not block the pseudonym until idle eviction.
209            let session_id = self.session_id;
210            warn!(%session_id, "rolling back partially established session slot after setup failure");
211            if let Some(slot) = self.sessions.remove(&session_id) {
212                self.active_sessions.fetch_sub(1, Ordering::Relaxed);
213
214                close_session(session_id, slot, ClosureReason::Eviction);
215            }
216        }
217    }
218}
219
220/// Indicates the result of processing a message.
221#[derive(Clone, Debug, PartialEq, Eq)]
222pub enum DispatchResult {
223    /// Session or Start protocol message has been processed successfully.
224    Processed,
225    /// The message was not related to Start or Session protocol.
226    Unrelated(ApplicationDataIn),
227}
228
229/// Configuration for the [`SessionManager`].
230#[derive(Clone, Debug, PartialEq, smart_default::SmartDefault)]
231pub struct SessionManagerConfig {
232    /// The maximum chunk of data that can be written to the Session's input buffer.
233    ///
234    /// Default is 1500.
235    #[default(1500)]
236    pub frame_mtu: usize,
237
238    /// The maximum time for an incomplete frame to stay in the Session's output buffer.
239    ///
240    /// Default is 800 ms.
241    #[default(Duration::from_millis(800))]
242    pub max_frame_timeout: Duration,
243
244    /// Maximum number of segments to buffer in the downstream transport of a Session's socket.
245    /// If 0 is given, the transport is unbuffered.
246    ///
247    /// Default is 0.
248    #[default(0)]
249    pub max_buffered_segments: usize,
250
251    /// The base timeout for initiation of Session initiation.
252    ///
253    /// The actual timeout is adjusted according to the number of hops for that Session:
254    /// `t = initiation_time_out_base * (num_forward_hops + num_return_hops + 2)`
255    ///
256    /// Default is 500 milliseconds.
257    #[default(Duration::from_millis(500))]
258    pub initiation_timeout_base: Duration,
259
260    /// Timeout for Session to be closed due to inactivity.
261    ///
262    /// Default is 180 seconds.
263    #[default(Duration::from_secs(180))]
264    pub idle_timeout: Duration,
265
266    /// The sampling interval for SURB balancer.
267    /// It will make SURB control decisions regularly at this interval.
268    ///
269    /// Default is 100 milliseconds.
270    #[default(Duration::from_millis(100))]
271    pub balancer_sampling_interval: Duration,
272
273    /// Initial packets per second egress rate on an incoming Session.
274    ///
275    /// This only applies to incoming Sessions without the [`Capability::NoRateControl`] flag set.
276    ///
277    /// Default is 10 packets/second.
278    #[default(10)]
279    pub initial_return_session_egress_rate: usize,
280
281    /// Minimum period of time for which a SURB buffer at the Exit must
282    /// endure if no SURBs are being received.
283    ///
284    /// In other words, it is the minimum period of time an Exit must withstand when
285    /// no SURBs are received from the Entry at all. To do so, the egress traffic
286    /// will be shaped accordingly to meet this requirement.
287    ///
288    /// This only applies to incoming Sessions without the [`Capability::NoRateControl`] flag set.
289    ///
290    /// Default is 5 seconds, minimum is 1 second.
291    #[default(Duration::from_secs(5))]
292    pub minimum_surb_buffer_duration: Duration,
293
294    /// Indicates the maximum number of SURBs in the SURB buffer to be requested when creating a new Session.
295    ///
296    /// This value is theoretically capped by the size of the global transport SURB ring buffer,
297    /// so values greater than that do not make sense. This value should be ideally set equal
298    /// to the size of the global transport SURB RB.
299    ///
300    /// Default is 10 000 SURBs.
301    #[default(10_000)]
302    pub maximum_surb_buffer_size: usize,
303
304    /// If set, the Session recipient (Exit) will notify the Session initiator (Entry) about
305    /// its SURB balance for the Session using keep-alive packets periodically.
306    ///
307    /// Keep in mind that each notification also costs 1 SURB, so the notification period should
308    /// not be too frequent.
309    ///
310    /// These notifications are the only absolute correction of the Entry's dead-reckoned
311    /// estimate of the Exit's SURB buffer. Without them, every packet lost in either
312    /// direction permanently inflates the Entry's estimate, until the Exit silently runs
313    /// out of SURBs and can no longer send any reply data.
314    ///
315    /// Default is 60 seconds (None disables the notifications), minimum is 1 second.
316    #[default(Some(Duration::from_secs(60)))]
317    pub surb_balance_notify_period: Option<Duration>,
318
319    /// If set, the Session initiator (Entry) will notify the Session recipient (Exit) about
320    /// the local SURB balancer target using keep-alive packets from the SURB balancer.
321    ///
322    /// This is useful when the client plans to change the SURB balancer target dynamically.
323    ///
324    /// Default is true.
325    #[default(true)]
326    pub surb_target_notify: bool,
327
328    /// Maximum number of concurrent sessions allowed.
329    ///
330    /// Default is 10_000.
331    #[default(10_000)]
332    pub maximum_sessions: usize,
333
334    /// How many packets can be buffered if the [`HoprSession`] input socket is not fast enough.
335    ///
336    /// Controls the capacity of the internal `crossfire` channel used for each session slot.
337    ///
338    /// Default is 10_000.
339    #[default(10000)]
340    pub session_forward_capacity: usize,
341}
342
343// Type-erased sink used by the `SessionManager` to notify about newly incoming sessions.
344// The errors produced by the underlying sink are remapped into `SessionManagerError`.
345type IncomingSessionSink = Pin<Box<dyn Sink<IncomingSession, Error = SessionManagerError> + Send>>;
346
347type SessionNotifiers = (
348    Arc<hopr_utils::runtime::prelude::Mutex<IncomingSessionSink>>,
349    crossfire::MTx<crossfire::mpsc::Array<(SessionId, ClosureReason)>>,
350);
351
352// Sink for processing Start protocol messages.
353// Must be within Arc to be shared across SessionManager clones.
354// The inner OnceLock is set once in `start()` and read in `dispatch_message`.
355type StartProtocolMsgSink = Arc<OnceLock<crossfire::MTx<crossfire::mpsc::Array<(HoprPseudonym, HoprStartProtocol)>>>>;
356
357/// Manages lifecycles of Sessions.
358///
359/// Once the manager is [started](SessionManager::start), the [`SessionManager::dispatch_message`]
360/// should be called for each [`ApplicationData`] received by the node.
361/// This way, the `SessionManager` takes care of proper Start sub-protocol message processing
362/// and correct dispatch of Session-related packets to individual existing Sessions.
363///
364/// Secondly, the manager can initiate new outgoing sessions via [`SessionManager::new_session`],
365/// probe sessions using [`SessionManager::ping_session`]
366/// and list them via [`SessionManager::active_sessions`].
367///
368/// Since the `SessionManager` operates over the HOPR protocol,
369/// the message transport `S` is required.
370/// Such transport must also be `Clone`, since it will be cloned into all the created [`HoprSession`] objects.
371///
372/// ## SURB balancing
373///
374/// The manager also can take care of automatic [SURB balancing](SurbBalancerConfig) per Session.
375///
376/// With each packet sent from the session initiator over to the receiving party, zero to 2 SURBs might be delivered.
377/// When the receiving party wants to send reply packets back, it must consume 1 SURB per packet. This
378/// means that if the difference between the SURBs delivered and SURBs consumed is negative, the receiving party
379/// might soon run out of SURBs. If SURBs run out, the reply packets will be dropped, causing likely quality of
380/// service degradation.
381///
382/// In an attempt to counter this effect, there are two co-existing automated modes of SURB balancing:
383/// *local SURB balancing* and *remote SURB balancing*.
384///
385/// ### Local SURB balancing
386///
387/// Local SURB balancing is performed on the sessions that were initiated by another party (and are
388/// therefore incoming to us).
389/// The local SURB balancing mechanism continuously evaluates the rate of SURB consumption and retrieval,
390/// and if SURBs are running out, the packet egress shaping takes effect. This by itself does not
391/// avoid the depletion of SURBs but slows it down in the hope that the initiating party can deliver
392/// more SURBs over time. This might happen either organically by sending effective payloads that
393/// allow non-zero number of SURBs in the packet, or non-organically by delivering KeepAlive messages
394/// via *remote SURB balancing*.
395///
396/// The egress shaping is done automatically, unless the Session initiator sets the [`Capability::NoRateControl`]
397/// flag during Session initiation.
398///
399/// ### Remote SURB balancing
400///
401/// Remote SURB balancing is performed by the Session initiator. The SURB balancer estimates the number of SURBs
402/// delivered to the other party, and also the number of SURBs consumed by seeing the amount of traffic received
403/// in replies.
404/// When enabled, a desired target level of SURBs at the Session counterparty is set. According to measured
405/// inflow and outflow of SURBs to/from the counterparty, the production of non-organic SURBs is started
406/// via keep-alive messages (sent to counterparty) and is controlled to maintain that target level.
407///
408/// In other words, the Session initiator tries to compensate for the usage of SURBs by the counterparty by
409/// sending new ones via the keep-alive messages.
410///
411/// This mechanism is configurable via the `surb_management` field in [`SessionClientConfig`].
412///
413/// ### Possible scenarios
414///
415/// There are 4 different scenarios of local vs. remote SURB balancing configuration, but
416/// an equilibrium (= matching the SURB production and consumption) is most likely to be reached
417/// only when both are configured (the ideal case below):
418///
419/// #### 1. Ideal local and remote SURB balancing
420///
421/// 1. The Session recipient (Exit) set the `initial_return_session_egress_rate`, `max_surb_buffer_duration` and
422///    `maximum_surb_buffer_size` values in the [`SessionManagerConfig`].
423/// 2. The Session initiator (Entry) sets the [`target_surb_buffer_size`](SurbBalancerConfig) which matches the
424///    [`maximum_surb_buffer_size`](SessionManagerConfig) of the counterparty.
425/// 3. The Session initiator (Entry) does *NOT* set the [`Capability::NoRateControl`] capability flag when opening
426///    Session.
427/// 4. The Session initiator (Entry) sets [`max_surbs_per_sec`](SurbBalancerConfig) slightly higher than the
428///    `maximum_surb_buffer_size / max_surb_buffer_duration` value configured at the counterparty.
429///
430/// In this situation, the maximum Session egress from Exit to the Entry is given by the
431/// `maximum_surb_buffer_size / max_surb_buffer_duration` ratio. If there is enough bandwidth,
432/// the (remote) SURB balancer sending SURBs to the Exit will stabilize roughly at this rate of SURBs/sec,
433/// and the whole system will be in equilibrium during the Session's lifetime (under ideal network conditions).
434///
435/// #### 2. Remote SURB balancing only
436///
437/// 1. The Session initiator (Entry) *DOES* set the [`Capability::NoRateControl`] capability flag when opening Session.
438/// 2. The Session initiator (Entry) sets `max_surbs_per_sec` and `target_surb_buffer_size` values in
439///    [`SurbBalancerConfig`]
440///
441/// In this one-sided situation, the Entry node floods the Exit node with SURBs,
442/// only based on its estimated consumption of SURBs at the Exit. The Exit's egress is not
443/// rate-limited at all. If the Exit runs out of SURBs at any point in time, it will simply drop egress packets.
444///
445/// This configuration could potentially only lead to an equilibrium
446/// when the `SurbBalancer` at the Entry can react fast enough to Exit's demand.
447///
448/// #### 3. Local SURB balancing only
449///
450/// 1. The Session recipient (Exit) set the `initial_return_session_egress_rate`, `max_surb_buffer_duration` and
451///    `maximum_surb_buffer_size` values in the [`SessionManagerConfig`].
452/// 2. The Session initiator (Entry) does *NOT* set the [`Capability::NoRateControl`] capability flag when opening
453///    Session.
454/// 3. The Session initiator (Entry) does *NOT* set the [`SurbBalancerConfig`] at all when opening Session.
455///
456/// In this one-sided situation, the Entry node does not provide any additional SURBs at all (except the
457/// ones that are naturally carried by the egress packets which have space to hold SURBs). It relies
458/// only on the Session egress limiting of the Exit node.
459/// The Exit will limit the egress roughly to the rate of natural SURB occurrence in the ingress.
460///
461/// This configuration could potentially only lead to an equilibrium when uploading non-full packets
462/// (ones that can carry at least a single SURB), and the Exit's egress is limiting itself to such a rate.
463/// If Exit's egress reaches low values due to SURB scarcity, the upper layer protocols over Session might break.
464///
465/// #### 4. No SURB balancing on each side
466///
467/// 1. The Session initiator (Entry) *DOES* set the [`Capability::NoRateControl`] capability flag when opening Session.
468/// 2. The Session initiator (Entry) does *NOT* set the [`SurbBalancerConfig`] at all when opening Session.
469///
470/// In this situation, no additional SURBs are being produced by the Entry and no Session egress rate-limiting
471/// takes place at the Exit.
472///
473/// This configuration can only lead to an equilibrium when Entry sends non-full packets (ones that carry
474/// at least a single SURB) and the Exit is consuming the SURBs (Session egress) at a slower or equal rate.
475/// Such configuration is very fragile, as any disturbances in the SURB flow might lead to a packet drop
476/// at the Exit's egress.
477///
478/// ### SURB decay
479///
480/// In a hypothetical scenario of a non-zero packet loss, the Session initiator (Entry) might send a
481/// certain number of SURBs to the Session recipient (Exit), but only a portion of it is actually delivered.
482/// The Entry has no way of knowing that and assumes that everything has been delivered.
483/// A similar problem happens when the Exit uses SURBs to construct return packets, but only a portion
484/// of those packets is actually delivered to the Entry. At this point, the Entry also subtracts
485/// fewer SURBs from its SURB estimate at the Exit.
486///
487/// In both situations, the Entry thinks there are more SURBs available at the Exit than there really are.
488///
489/// To compensate for a potential packet loss, the Entry's estimation of Exit's SURB buffer is regularly
490/// diminished by a percentage of the `target_surb_buffer_size`, even if no incoming traffic from the
491/// Exit is detected.
492///
493/// This behavior can be controlled via the `surb_decay` field of [`SurbBalancerConfig`].
494///
495/// ### SURB balance and target notification
496///
497/// The Session recipient (Exit) can notify the Session initiator (Entry) periodically about its estimated
498/// number of SURBs for the Session. This can help the Entry to adjust its approximation of that level so
499/// that its Local SURB balancer can better intervene.
500/// This can be set using the `surb_balance_notify_period` field of [`SessionManagerConfig`] for the Exit.
501///
502/// Likewise, the Entry can inform the Exit about its desired SURB buffer target so that the Exit
503/// can better accommodate its Remote SURB balancing.
504/// This can be set using the `surb_target_notify` field of the [`SessionManagerConfig`] of each new Session.
505///
506/// Both mechanisms leverage the Keep Alive message to report the respective values.
507pub struct SessionManager<S> {
508    session_initiations: SessionInitiationCache,
509    session_notifiers: Arc<OnceLock<SessionNotifiers>>,
510    start_protocol_tx: StartProtocolMsgSink,
511    /// Authoritative session count for admission control.
512    /// Incremented atomically inside `allocate_session_slot` before the cache insertion,
513    /// and decremented at every removal path (explicit close, eviction, guard rollback).
514    active_sessions: Arc<std::sync::atomic::AtomicUsize>,
515    sessions: moka::sync::Cache<SessionId, SessionSlot>,
516    msg_sender: Arc<OnceLock<S>>,
517    cfg: SessionManagerConfig,
518}
519
520impl<S> Clone for SessionManager<S> {
521    fn clone(&self) -> Self {
522        Self {
523            session_initiations: self.session_initiations.clone(),
524            session_notifiers: self.session_notifiers.clone(),
525            start_protocol_tx: self.start_protocol_tx.clone(),
526            active_sessions: self.active_sessions.clone(),
527            sessions: self.sessions.clone(),
528            cfg: self.cfg.clone(),
529            msg_sender: self.msg_sender.clone(),
530        }
531    }
532}
533
534fn session_config(cfg: &SessionManagerConfig, capabilities: crate::Capabilities) -> HoprSessionConfig {
535    HoprSessionConfig {
536        capabilities,
537        frame_mtu: cfg.frame_mtu,
538        frame_timeout: cfg.max_frame_timeout,
539        max_buffered_segments: cfg.max_buffered_segments,
540    }
541}
542
543#[cfg(feature = "telemetry")]
544fn initialize_session_telemetry(
545    session_id: SessionId,
546    cfg: &SessionManagerConfig,
547    capabilities: crate::Capabilities,
548    surb_estimator: Option<&AtomicSurbFlowEstimator>,
549    surb_mgmt: Option<&Arc<BalancerStateValues>>,
550) {
551    initialize_session_metrics(session_id, session_config(cfg, capabilities));
552    set_session_state(&session_id, SessionLifecycleState::Active);
553    if let (Some(estimator), Some(mgmt)) = (surb_estimator, surb_mgmt) {
554        set_session_balancer_data(&session_id, estimator.clone(), mgmt.clone());
555    }
556}
557
558async fn send_via_msg_sender<S, D>(
559    msg_sender: &mut S,
560    routing: DestinationRouting,
561    data: D,
562    error_context: &'static str,
563) -> crate::errors::Result<()>
564where
565    S: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Unpin,
566    S::Error: std::error::Error + Send + Sync + Clone + 'static,
567    D: TryInto<ApplicationData>,
568    D::Error: std::error::Error + Send + Sync + 'static,
569{
570    let app_data: ApplicationData = data.try_into().map_err(SessionManagerError::other)?;
571    msg_sender
572        .send((routing, ApplicationDataOut::with_no_packet_info(app_data)))
573        .timeout(futures_time::time::Duration::from(EXTERNAL_SEND_TIMEOUT))
574        .await
575        .map_err(|_| {
576            error!("timeout sending {error_context}");
577            TransportSessionError::Timeout
578        })?
579        .map_err(|error| {
580            error!(%error, "failed to send {error_context}");
581            SessionManagerError::other(error)
582        })?;
583    Ok(())
584}
585
586impl<S> SessionManager<S>
587where
588    S: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Clone + Send + Sync + Unpin + 'static,
589    S::Error: std::error::Error + Send + Sync + Clone + 'static,
590{
591    /// Creates a new instance given the [`config`](SessionManagerConfig).
592    pub fn new(mut cfg: SessionManagerConfig) -> Self {
593        let maximum_sessions = cfg.maximum_sessions;
594        cfg.surb_balance_notify_period = cfg
595            .surb_balance_notify_period
596            .map(|p| p.max(MIN_SURB_BUFFER_NOTIFICATION_PERIOD));
597        cfg.minimum_surb_buffer_duration = cfg.minimum_surb_buffer_duration.max(MIN_SURB_BUFFER_DURATION);
598
599        // Ensure the Frame MTU is at least the size of the Session segment MTU payload
600        cfg.frame_mtu = cfg.frame_mtu.max(SESSION_MTU);
601        cfg.max_frame_timeout = cfg.max_frame_timeout.max(MIN_FRAME_TIMEOUT);
602
603        #[cfg(all(feature = "telemetry", not(test)))]
604        METRIC_ACTIVE_SESSIONS.set(0.0);
605
606        let active_sessions: Arc<std::sync::atomic::AtomicUsize> = Arc::new(std::sync::atomic::AtomicUsize::new(0));
607        let active_sessions_for_listener = active_sessions.clone();
608
609        let msg_sender = Arc::new(OnceLock::new());
610        Self {
611            msg_sender: msg_sender.clone(),
612            session_initiations: moka::sync::Cache::builder()
613                .max_capacity(maximum_sessions as u64)
614                .time_to_live(
615                    2 * initiation_timeout_max_one_way(
616                        cfg.initiation_timeout_base,
617                        RoutingOptions::MAX_INTERMEDIATE_HOPS,
618                    ),
619                )
620                .build(),
621            sessions: moka::sync::Cache::builder()
622                .max_capacity(maximum_sessions as u64)
623                .time_to_idle(cfg.idle_timeout)
624                .eviction_listener(move |session_id: Arc<SessionId>, entry, reason| match &reason {
625                    moka::notification::RemovalCause::Expired | moka::notification::RemovalCause::Size => {
626                        trace!(?session_id, ?reason, "session evicted from the cache");
627                        active_sessions_for_listener.fetch_sub(1, Ordering::Relaxed);
628                        close_session(*session_id.as_ref(), entry, ClosureReason::Eviction);
629                    }
630                    _ => {}
631                })
632                .build(),
633            session_notifiers: Arc::new(OnceLock::new()),
634            start_protocol_tx: Arc::new(OnceLock::new()),
635            active_sessions,
636            cfg,
637        }
638    }
639
640    /// Starts the instance with the given `msg_sender` `Sink`
641    /// and a channel `new_session_notifier` used to notify when a new incoming session is opened to us.
642    ///
643    /// This method must be called prior to any calls to [`SessionManager::new_session`] or
644    /// [`SessionManager::dispatch_message`].
645    pub fn start<T>(&self, msg_sender: S, new_session_notifier: T) -> crate::errors::Result<Vec<AbortHandle>>
646    where
647        T: futures::Sink<IncomingSession> + Send + 'static,
648        T::Error: std::error::Error + Send + Sync + 'static,
649    {
650        self.msg_sender
651            .set(msg_sender)
652            .map_err(|_| SessionManagerError::AlreadyStarted)?;
653
654        // Re-map the user-provided sink errors to `SessionManagerError` and erase the concrete
655        // type, so that the `SessionManager` does not need to be generic over it. This also avoids
656        // having to spawn a separate task to forward items between channels: senders simply lock
657        // the sink and send directly.
658        let new_session_notifier: IncomingSessionSink =
659            Box::pin(new_session_notifier.sink_map_err(SessionManagerError::other));
660        let new_session_notifier = Arc::new(hopr_utils::runtime::prelude::Mutex::new(new_session_notifier));
661
662        let (session_close_tx, session_close_rx) =
663            crossfire::mpsc::bounded_blocking_async(self.cfg.maximum_sessions + 10);
664        self.session_notifiers
665            .set((new_session_notifier, session_close_tx))
666            .map_err(|_| SessionManagerError::AlreadyStarted)?;
667
668        let (start_protocol_tx, start_protocol_rx) =
669            crossfire::mpsc::bounded_blocking_async(self.cfg.maximum_sessions + 10);
670        let _ = self.start_protocol_tx.set(start_protocol_tx);
671
672        let myself = self.clone();
673        let closure_diag = hopr_utils::runtime::diagnostics::ConcurrentDiagnostics::new(
674            "session_close_for_each_concurrent",
675            module_path!(),
676            file!(),
677            line!(),
678        );
679        let ah_closure_notifications = hopr_utils::spawn_as_abortable_named!(
680            "session_close_notifications",
681            session_close_rx.into_stream().for_each_concurrent(
682                self.cfg.maximum_sessions + 10,
683                move |(session_id, closure_reason)| {
684                    let myself = myself.clone();
685                    let closure_diag = closure_diag.clone();
686                    closure_diag.wrap(|| {
687                        // These notifications come from the Sessions themselves once
688                        // an empty read is encountered, which means the closure was done by the
689                        // other party.
690                        if let Some(session_data) = myself.sessions.remove(&session_id) {
691                            myself.active_sessions.fetch_sub(1, Ordering::Relaxed);
692                            close_session(session_id, session_data, closure_reason);
693                        } else {
694                            // Do not treat this as an error
695                            debug!(
696                                ?session_id,
697                                ?closure_reason,
698                                "could not find session id to close, maybe the session is already closed"
699                            );
700                        }
701                        futures::future::ready(())
702                    })
703                }
704            )
705        );
706
707        // This is necessary to evict expired entries from the caches if
708        // no session-related operations happen at all.
709        // This ensures the dangling expired sessions are properly closed
710        // and their closure is timely notified to the other party.
711        let myself = self.clone();
712        let ah_session_expiration = hopr_utils::spawn_as_abortable!(async move {
713            let jitter = hopr_api::types::crypto_random::random_float_in_range(1.0..1.5);
714            let timeout = 2 * initiation_timeout_max_one_way(
715                myself.cfg.initiation_timeout_base,
716                RoutingOptions::MAX_INTERMEDIATE_HOPS,
717            )
718            .min(myself.cfg.idle_timeout)
719            .mul_f64(jitter)
720                / 2;
721            futures_time::stream::interval(timeout.into())
722                .for_each(|_| async {
723                    trace!("executing session cache evictions");
724                    myself.sessions.run_pending_tasks();
725                    myself.session_initiations.run_pending_tasks();
726                })
727                .await;
728        });
729
730        // Begin processing of Start protocol messages
731        let myself = self.clone();
732        let ah_start_protocol = hopr_utils::spawn_as_abortable_named!(
733            "session_start_protocol_processor",
734            start_protocol_rx.into_stream().for_each_concurrent(
735                Some(self.cfg.maximum_sessions + 10),
736                move |(pseudonym, protocol_msg)| {
737                    let myself = myself.clone();
738                    async move {
739                        let result = match protocol_msg {
740                            HoprStartProtocol::StartSession(session_req) => {
741                                myself.handle_incoming_session_initiation(pseudonym, session_req).await
742                            }
743                            HoprStartProtocol::SessionEstablished(est) => myself.handle_session_established(est).await,
744                            HoprStartProtocol::SessionError(error_type) => {
745                                myself.handle_session_error(error_type).await
746                            }
747                            HoprStartProtocol::KeepAlive(msg) => myself.handle_keep_alive(msg).await,
748                        };
749
750                        if let Err(error) = result {
751                            error!(%error, "failed to process Start protocol message");
752                        }
753                    }
754                }
755            )
756        );
757
758        Ok(vec![ah_closure_notifications, ah_session_expiration, ah_start_protocol])
759    }
760
761    /// Check if [`start`](SessionManager::start) has been called and the instance is running.
762    pub fn is_started(&self) -> bool {
763        self.session_notifiers.get().is_some()
764    }
765
766    /// Atomically allocates a new [`SessionSlot`] for `session_id` and returns an RAII
767    /// [`SessionSlotGuard`] for it.
768    ///
769    /// Establishing a session involves several fallible steps *after* the slot has been
770    /// inserted. The returned guard rolls the slot back - tearing the partially
771    /// established session down via [`close_session`] - unless it is
772    /// [committed](SessionSlotGuard::commit).
773    ///
774    /// The active-sessions gauge is incremented here, atomically with the insertion and
775    /// the guard creation, precisely so that it is always paired with the guard's
776    /// rollback decrement (performed through [`close_session`]). This keeps the gauge
777    /// accurate: it is never decremented for a slot that was not counted in the first
778    /// place, and every counted slot is decremented exactly once when it leaves the cache.
779    ///
780    /// Returns `None` if a slot for `session_id` already exists; in that case nothing is
781    /// inserted, the gauge is left untouched, and no guard is produced. The atomic `entry`
782    /// API guarantees that only one concurrent caller can claim the slot for a given
783    /// pseudonym (avoiding a TOCTOU race), which also rules out loopback sessions onto
784    /// ourselves.
785    ///
786    /// Capacity is enforced by an atomic counter incremented *before* the cache insertion,
787    /// making it impossible for two concurrent callers (with different session IDs) to both
788    /// succeed when the cache is already at `maximum_sessions`.
789    fn allocate_session_slot(&self, session_id: SessionId, slot: SessionSlot) -> Option<SessionSlotGuard<'_>> {
790        // Try to claim a session slot before touching the cache. `fetch_update` atomically
791        // increments only if the value is strictly below the limit, preventing two concurrent
792        // callers from both succeeding when already at capacity.
793        let counter = &self.active_sessions;
794        #[allow(clippy::incompatible_msrv)]
795        let did_reserve = counter
796            .try_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
797                (n < self.cfg.maximum_sessions).then_some(n + 1)
798            })
799            .is_ok();
800
801        if !did_reserve {
802            return None;
803        }
804
805        let result =
806            self.sessions
807                .entry(session_id)
808                .and_compute_with(|entry: Option<moka::Entry<SessionId, SessionSlot>>| {
809                    if entry.is_none() {
810                        moka::ops::compute::Op::Put(slot)
811                    } else {
812                        // Duplicate key — release the reservation so the counter stays accurate.
813                        counter.fetch_sub(1, Ordering::Relaxed);
814                        moka::ops::compute::Op::Nop
815                    }
816                });
817
818        match result {
819            moka::ops::compute::CompResult::Inserted(_) => {
820                // take_guard borrows self, so the guard stores the counter clone separately.
821                Some(SessionSlotGuard::new(&self.sessions, session_id, counter.clone()))
822            }
823            _ => None,
824        }
825    }
826
827    /// Initiates a new outgoing Session to `destination` with the given configuration.
828    ///
829    /// If the Session's counterparty does not respond within
830    /// the [configured](SessionManagerConfig) period,
831    /// this method returns [`TransportSessionError::Timeout`].
832    ///
833    /// It will also fail if the instance has not been [started](SessionManager::start).
834    pub async fn new_session(
835        &self,
836        destination: Address,
837        target: SessionTarget,
838        cfg: SessionClientConfig,
839    ) -> crate::errors::Result<HoprSession> {
840        self.sessions.run_pending_tasks();
841        if self.cfg.maximum_sessions <= self.active_sessions.load(Ordering::Relaxed) {
842            return Err(SessionManagerError::TooManySessions.into());
843        }
844
845        let mut msg_sender = self.msg_sender.get().cloned().ok_or(SessionManagerError::NotStarted)?;
846
847        let (tx_initiation_done, rx_initiation_done): (
848            crossfire::MTx<crossfire::mpsc::One<_>>,
849            crossfire::AsyncRx<crossfire::mpsc::One<_>>,
850        ) = crossfire::mpsc::build(crossfire::mpsc::One::new());
851
852        let (challenge, _) = insert_into_next_slot(
853            &self.session_initiations,
854            |ch| {
855                if let Some(challenge) = ch {
856                    ((challenge + 1) % hopr_api::types::crypto_random::MAX_RANDOM_INTEGER).max(MIN_CHALLENGE)
857                } else {
858                    hopr_api::types::crypto_random::random_integer(MIN_CHALLENGE, None)
859                }
860            },
861            |_| tx_initiation_done,
862            Some(self.cfg.maximum_sessions as u64),
863        )
864        .ok_or(SessionManagerError::NoChallengeSlots)?; // almost impossible with u64
865
866        // Prepare the session initiation message in the Start protocol
867        trace!(challenge, ?cfg, "initiating session with config");
868        let start_session_msg = HoprStartProtocol::StartSession(StartInitiation {
869            challenge,
870            target,
871            capabilities: ByteCapabilities(cfg.capabilities),
872            additional_data: if !cfg.capabilities.contains(Capability::NoRateControl) {
873                cfg.surb_management
874                    .map(|c| c.target_surb_buffer_size)
875                    .unwrap_or(
876                        self.cfg.initial_return_session_egress_rate as u64
877                            * self
878                                .cfg
879                                .minimum_surb_buffer_duration
880                                .max(MIN_SURB_BUFFER_DURATION)
881                                .as_secs(),
882                    )
883                    .min(u32::MAX as u64) as u32
884            } else {
885                0
886            },
887        });
888
889        let pseudonym = cfg.pseudonym.unwrap_or(HoprPseudonym::random());
890        let forward_routing = DestinationRouting::Forward {
891            destination: Box::new(destination.into()),
892            pseudonym: Some(pseudonym), // Session must use a fixed pseudonym already
893            forward_options: cfg.forward_path_options.clone(),
894            return_options: cfg.return_path_options.clone().into(),
895        };
896
897        // Send the Session initiation message
898        info!(challenge, %pseudonym, %destination, "new session request");
899        send_via_msg_sender(
900            &mut msg_sender,
901            forward_routing.clone(),
902            start_session_msg,
903            "session request message",
904        )
905        .await
906        .map_err(|error| {
907            self.session_initiations.remove(&challenge);
908            TransportSessionError::packet_sending(error)
909        })?;
910
911        // The timeout is given by the number of hops requested
912        let initiation_timeout: futures_time::time::Duration = initiation_timeout_max_one_way(
913            self.cfg.initiation_timeout_base,
914            cfg.forward_path_options.count_hops() + cfg.return_path_options.count_hops() + 2,
915        )
916        .into();
917
918        // Await session establishment response from the Exit node or timeout
919
920        trace!(challenge, "awaiting session establishment");
921        match rx_initiation_done
922            .into_stream()
923            .try_next()
924            .timeout(initiation_timeout)
925            .await
926        {
927            Ok(Ok(Some(est))) => {
928                // Session has been established, construct it
929                let session_id = est.session_id;
930                debug!(challenge = est.orig_challenge, ?session_id, "started a new session");
931
932                let (session_tx, session_rx) =
933                    crossfire::mpsc::bounded_blocking_async::<ApplicationDataIn>(self.cfg.session_forward_capacity);
934                let (session_rx, session_rx_ah) = hopr_utils::runtime::DropAbortable::new(session_rx.into_stream());
935
936                let mut abort_handles = AbortableList::default();
937                abort_handles.insert(SessionHandles::Ingress, session_rx_ah);
938
939                let notifier = self
940                    .session_notifiers
941                    .get()
942                    .map(|(_, notifier)| {
943                        let notifier = notifier.clone();
944                        Box::new(move |session_id: SessionId, reason: ClosureReason| {
945                            let _ = notifier
946                                .try_send((session_id, reason))
947                                .inspect_err(|error| error!(%session_id, %error, "failed to notify session closure"));
948                        })
949                    })
950                    .ok_or(SessionManagerError::NotStarted)?;
951
952                // NOTE: the Exit node can have different `max_surb_buffer_size`
953                // setting on the Session manager, so it does not make sense to cap it here
954                // with our maximum value.
955                if let Some(balancer_config) = cfg.surb_management {
956                    let surb_estimator = AtomicSurbFlowEstimator::default();
957
958                    // Sender responsible for keep-alive and Session data will be counting produced SURBs
959                    let surb_estimator_clone = surb_estimator.clone();
960                    let full_surb_scoring_sender =
961                        msg_sender.with(move |(routing, data): (DestinationRouting, ApplicationDataOut)| {
962                            let produced = data.estimate_surbs_with_msg() as u64;
963                            // Count how many SURBs we sent with each packet
964                            surb_estimator_clone
965                                .produced
966                                .fetch_add(produced, std::sync::atomic::Ordering::Relaxed);
967                            #[cfg(feature = "telemetry")]
968                            crate::telemetry::record_session_surb_produced(&session_id, produced);
969                            futures::future::ok::<_, S::Error>((routing, data))
970                        });
971
972                    // For standard Session data we first reduce the number of SURBs we want to produce,
973                    // unless requested to always max them out
974                    let max_out_organic_surbs = cfg.always_max_out_surbs;
975                    let reduced_surb_scoring_sender = full_surb_scoring_sender.clone().with(
976                        // NOTE: this is put in-front of the `full_surb_scoring_sender`,
977                        // so that its estimate of SURBs gets automatically updated based on
978                        // the `max_surbs_in_packets` set here.
979                        move |(routing, mut data): (DestinationRouting, ApplicationDataOut)| {
980                            if !max_out_organic_surbs {
981                                // TODO: make this dynamic to honor the balancer target (#7439)
982                                data.packet_info
983                                    .get_or_insert_with(|| OutgoingPacketInfo {
984                                        max_surbs_in_packet: 1,
985                                        ..Default::default()
986                                    })
987                                    .max_surbs_in_packet = 1;
988                            }
989                            futures::future::ok::<_, S::Error>((routing, data))
990                        },
991                    );
992
993                    let surb_mgmt = Arc::new(BalancerStateValues::from(balancer_config));
994
995                    // Spawn the SURB-bearing keep alive stream towards the Exit
996                    let (ka_controller, ka_abort_handle) = utils::spawn_keep_alive_stream(
997                        session_id,
998                        full_surb_scoring_sender,
999                        forward_routing.clone(),
1000                        if self.cfg.surb_target_notify {
1001                            SurbNotificationMode::Target
1002                        } else {
1003                            SurbNotificationMode::DoNotNotify
1004                        },
1005                        surb_mgmt.clone(),
1006                    );
1007                    abort_handles.insert(SessionHandles::KeepAlive, ka_abort_handle);
1008
1009                    // Spawn the SURB balancer, which will decide on the initial SURB rate.
1010                    debug!(%session_id, ?balancer_config ,"spawning entry SURB balancer");
1011                    let balancer = SurbBalancer::new(
1012                        session_id,
1013                        // The setpoint and output limit is immediately reconfigured by the SurbBalancer
1014                        PidBalancerController::from_gains(PidControllerGains::from_env_or_default()),
1015                        surb_estimator.clone(),
1016                        // Currently, a keep-alive message can bear `HoprPacket::MAX_SURBS_IN_PACKET` SURBs,
1017                        // so the correction by this factor is applied.
1018                        SurbControllerWithCorrection(ka_controller, HoprPacket::MAX_SURBS_IN_PACKET as u32),
1019                        surb_mgmt.clone(),
1020                    );
1021
1022                    let (level_stream, balancer_abort_handle) =
1023                        balancer.start_control_loop(self.cfg.balancer_sampling_interval);
1024                    abort_handles.insert(SessionHandles::Balancer, balancer_abort_handle);
1025
1026                    // Insert the slot and obtain a guard that rolls it back (also tearing
1027                    // down the abort handles) if any subsequent setup step fails.
1028                    let mut slot_guard = self
1029                        .allocate_session_slot(
1030                            session_id,
1031                            SessionSlot {
1032                                session_tx,
1033                                routing_opts: forward_routing.clone(),
1034                                abort_handles: Arc::new(parking_lot::Mutex::new(abort_handles)),
1035                                surb_mgmt: surb_mgmt.clone(),
1036                                surb_estimator: surb_estimator.clone(),
1037                            },
1038                        )
1039                        .ok_or_else(|| {
1040                            // Session already exists; it means it is most likely a loopback attempt
1041                            error!(%session_id, "session already exists - loopback attempt");
1042                            SessionManagerError::Loopback
1043                        })?;
1044
1045                    #[cfg(all(feature = "telemetry", not(test)))]
1046                    METRIC_NUM_INITIATED_SESSIONS.increment();
1047
1048                    // Wait for enough SURBs to be sent to the counterparty
1049                    // TODO: consider making this interactive = other party reports the exact level periodically
1050                    match level_stream
1051                        .skip_while(|current_level| {
1052                            futures::future::ready(*current_level < balancer_config.target_surb_buffer_size / 2)
1053                        })
1054                        .next()
1055                        .timeout(futures_time::time::Duration::from(SESSION_READINESS_TIMEOUT))
1056                        .await
1057                    {
1058                        Ok(Some(surb_level)) => {
1059                            info!(%session_id, surb_level, "session is ready");
1060                        }
1061                        Ok(None) => {
1062                            return Err(
1063                                SessionManagerError::other(anyhow!("surb balancer was cancelled prematurely")).into(),
1064                            );
1065                        }
1066                        Err(_) => {
1067                            warn!(%session_id, "session didn't reach target SURB buffer size in time");
1068                        }
1069                    }
1070
1071                    let surb_estimator_for_rx = surb_estimator.clone();
1072                    let session = HoprSession::new(
1073                        session_id,
1074                        forward_routing,
1075                        session_config(&self.cfg, cfg.capabilities),
1076                        (
1077                            reduced_surb_scoring_sender,
1078                            session_rx.inspect(move |_| {
1079                                // Received packets = SURB consumption estimate
1080                                // The received packets always consume a single SURB.
1081                                surb_estimator_for_rx
1082                                    .consumed
1083                                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1084                                #[cfg(feature = "telemetry")]
1085                                crate::telemetry::record_session_surb_consumed(&session_id, 1);
1086                            }),
1087                        ),
1088                        Some(notifier),
1089                    )?;
1090
1091                    #[cfg(feature = "telemetry")]
1092                    initialize_session_telemetry(
1093                        session_id,
1094                        &self.cfg,
1095                        cfg.capabilities,
1096                        Some(&surb_estimator),
1097                        Some(&surb_mgmt),
1098                    );
1099
1100                    slot_guard.commit();
1101                    Ok(session)
1102                } else {
1103                    warn!(%session_id, "session ready without SURB balancing");
1104
1105                    // Insert the slot and obtain a guard that rolls it back if any
1106                    // subsequent setup step fails.
1107                    let mut slot_guard = self
1108                        .allocate_session_slot(
1109                            session_id,
1110                            SessionSlot {
1111                                session_tx,
1112                                routing_opts: forward_routing.clone(),
1113                                abort_handles: Arc::new(parking_lot::Mutex::new(abort_handles)),
1114                                surb_mgmt: Default::default(), // Disabled SURB management
1115                                surb_estimator: Default::default(), // No SURB estimator needed
1116                            },
1117                        )
1118                        .ok_or_else(|| {
1119                            // Session already exists; it means it is most likely a loopback attempt
1120                            error!(%session_id, "session already exists - loopback attempt");
1121                            SessionManagerError::Loopback
1122                        })?;
1123
1124                    #[cfg(all(feature = "telemetry", not(test)))]
1125                    METRIC_NUM_INITIATED_SESSIONS.increment();
1126
1127                    // For standard Session data we first reduce the number of SURBs we want to produce,
1128                    // unless requested to always max them out
1129                    let max_out_organic_surbs = cfg.always_max_out_surbs;
1130                    let reduced_surb_sender =
1131                        msg_sender.with(move |(routing, mut data): (DestinationRouting, ApplicationDataOut)| {
1132                            if !max_out_organic_surbs {
1133                                data.packet_info
1134                                    .get_or_insert_with(|| OutgoingPacketInfo {
1135                                        max_surbs_in_packet: 1,
1136                                        ..Default::default()
1137                                    })
1138                                    .max_surbs_in_packet = 1;
1139                            }
1140                            futures::future::ok::<_, S::Error>((routing, data))
1141                        });
1142
1143                    let session = HoprSession::new(
1144                        session_id,
1145                        forward_routing,
1146                        session_config(&self.cfg, cfg.capabilities),
1147                        (reduced_surb_sender, session_rx),
1148                        Some(notifier),
1149                    )?;
1150
1151                    #[cfg(feature = "telemetry")]
1152                    initialize_session_telemetry(session_id, &self.cfg, cfg.capabilities, None, None);
1153
1154                    slot_guard.commit();
1155                    Ok(session)
1156                }
1157            }
1158            Ok(Ok(None)) => {
1159                self.session_initiations.remove(&challenge);
1160                Err(SessionManagerError::other(anyhow!(
1161                    "internal error: sender has been closed without completing the session establishment"
1162                ))
1163                .into())
1164            }
1165            Ok(Err(error)) => {
1166                // The other side did not allow us to establish a session
1167                error!(
1168                    challenge = error.challenge,
1169                    ?error,
1170                    "the other party rejected the session initiation with error"
1171                );
1172                Err(TransportSessionError::Rejected(error.reason))
1173            }
1174            Err(_) => {
1175                // Timeout waiting for a session establishment
1176                error!(challenge, "session initiation attempt timed out");
1177
1178                #[cfg(all(feature = "telemetry", not(test)))]
1179                METRIC_RECEIVED_SESSION_ERRS.increment(&["timeout"]);
1180
1181                self.session_initiations.remove(&challenge);
1182                Err(TransportSessionError::Timeout)
1183            }
1184        }
1185    }
1186
1187    /// Sends a keep-alive packet with the given [`SessionId`].
1188    ///
1189    /// This currently "fires & forgets" and does not expect nor await any "pong" response.
1190    pub async fn ping_session(&self, id: &SessionId) -> crate::errors::Result<()> {
1191        if let Some(session_data) = self.sessions.get(id) {
1192            trace!(session_id = ?id, "pinging manually session");
1193            let mut msg_sender = self.msg_sender.get().cloned().ok_or(SessionManagerError::NotStarted)?;
1194            send_via_msg_sender(
1195                &mut msg_sender,
1196                session_data.routing_opts.clone(),
1197                HoprStartProtocol::KeepAlive((*id).into()),
1198                "session ping message",
1199            )
1200            .await
1201            .map_err(TransportSessionError::packet_sending)
1202        } else {
1203            Err(SessionManagerError::NonExistingSession.into())
1204        }
1205    }
1206
1207    /// Returns [`SessionIds`](SessionId) of all currently active sessions.
1208    pub fn active_sessions(&self) -> Vec<SessionId> {
1209        self.sessions.run_pending_tasks();
1210        self.sessions.iter().map(|(k, _)| *k).collect()
1211    }
1212
1213    /// Explicitly closes the session with the given `id`.
1214    ///
1215    /// Removes the entry from the internal session cache, closes the data channel,
1216    /// and aborts any auxiliary tasks. Returns `true` if a session was found and
1217    /// closed, `false` otherwise.
1218    ///
1219    /// This avoids waiting for the idle timeout (`time_to_idle`) or the LRU
1220    /// capacity bound to evict the entry, which is the desired behaviour when
1221    /// the caller (e.g. REST `DELETE /session`) knows the session is finished.
1222    pub fn close_session(&self, id: &SessionId) -> bool {
1223        if let Some(slot) = self.sessions.remove(id) {
1224            self.active_sessions.fetch_sub(1, Ordering::Relaxed);
1225            close_session(*id, slot, ClosureReason::Eviction);
1226            true
1227        } else {
1228            false
1229        }
1230    }
1231
1232    /// Updates the configuration of the SURB balancer on the given [`SessionId`].
1233    ///
1234    /// Returns an error if the Session with the given `id` does not exist, or
1235    /// if it does not use SURB balancing.
1236    pub fn update_surb_balancer_config(&self, id: &SessionId, config: SurbBalancerConfig) -> crate::errors::Result<()> {
1237        let cfg = self
1238            .sessions
1239            .get(id)
1240            .ok_or(SessionManagerError::NonExistingSession)?
1241            .surb_mgmt;
1242
1243        // Only update the config if there already was one before
1244        if !cfg.is_disabled() {
1245            cfg.update(&config);
1246            Ok(())
1247        } else {
1248            Err(SessionManagerError::other(anyhow!("session does not use SURB balancing")).into())
1249        }
1250    }
1251
1252    /// Retrieves the configuration of SURB balancing for the given Session.
1253    ///
1254    /// Returns an error if the Session with the given `id` does not exist.
1255    pub fn get_surb_balancer_config(&self, id: &SessionId) -> crate::errors::Result<Option<SurbBalancerConfig>> {
1256        match self.sessions.get(id) {
1257            Some(session) => Ok(Some(session.surb_mgmt.as_ref())
1258                .filter(|c| !c.is_disabled())
1259                .map(|d| d.as_config())),
1260            None => Err(SessionManagerError::NonExistingSession.into()),
1261        }
1262    }
1263
1264    /// Gets estimations produced/received and consumed SURBs by the Session.
1265    ///
1266    /// For an outgoing Session (Entry) the pair is the number of SURBs sent (by us) and used (by the Exit).
1267    /// For an incoming Session (Exit) the pair is the number of SURBs received (from Entry) and used (by us).
1268    ///
1269    /// Returns an error if the Session with the given `id` does not exist.
1270    pub fn get_surb_level_estimates(&self, id: &SessionId) -> crate::errors::Result<(u64, u64)> {
1271        match self.sessions.get(id) {
1272            Some(session) => Ok((
1273                session
1274                    .surb_estimator
1275                    .produced
1276                    .load(std::sync::atomic::Ordering::Relaxed),
1277                session
1278                    .surb_estimator
1279                    .consumed
1280                    .load(std::sync::atomic::Ordering::Relaxed),
1281            )),
1282            None => Err(SessionManagerError::NonExistingSession.into()),
1283        }
1284    }
1285
1286    /// The main method to be called whenever data are received.
1287    ///
1288    /// It tries to recognize the message and correctly dispatches either
1289    /// the Session protocol or Start protocol messages.
1290    ///
1291    /// If the data are not recognized, they are returned as [`DispatchResult::Unrelated`].
1292    pub fn dispatch_message(
1293        &self,
1294        pseudonym: HoprPseudonym,
1295        in_data: ApplicationDataIn,
1296    ) -> crate::errors::Result<DispatchResult> {
1297        if in_data.data.application_tag == HoprStartProtocol::START_PROTOCOL_MESSAGE_TAG {
1298            // This is a Start protocol message, so we send it to the handler
1299            trace!("dispatching Start protocol message");
1300            if let Some(start_protocol_tx) = self.start_protocol_tx.get() {
1301                start_protocol_tx
1302                    .try_send((pseudonym, HoprStartProtocol::try_from(in_data.data)?))
1303                    .map_err(|error| {
1304                        error!(%error, "failed to send Start protocol message to processing task");
1305                        SessionManagerError::other(error)
1306                    })?;
1307            } else {
1308                return Err(SessionManagerError::NotStarted.into());
1309            }
1310
1311            #[cfg(all(feature = "telemetry", not(test)))]
1312            METRIC_DISPATCHED_MSGS.increment_by(&["processed"], 1);
1313
1314            return Ok(DispatchResult::Processed);
1315        } else if in_data.data.application_tag == SESSION_APPLICATION_TAG {
1316            let session_id = pseudonym;
1317
1318            return if let Some(session_slot) = self.sessions.get(&session_id) {
1319                trace!(%session_id, "received data for a registered session");
1320
1321                Ok(session_slot
1322                    .session_tx
1323                    .try_send(in_data)
1324                    .map(|_| {
1325                        #[cfg(all(feature = "telemetry", not(test)))]
1326                        METRIC_DISPATCHED_MSGS.increment_by(&["processed"], 1);
1327
1328                        DispatchResult::Processed
1329                    })
1330                    .map_err(|error| {
1331                        error!(%session_id, %error, "failed to dispatch session data");
1332                        SessionManagerError::other(error)
1333                    })?)
1334            } else {
1335                error!(%session_id, "received data from an unestablished session");
1336                Err(TransportSessionError::UnknownData)
1337            };
1338        }
1339
1340        trace!(tag = %in_data.data.application_tag, "received data not associated with session protocol or any existing session");
1341
1342        #[cfg(all(feature = "telemetry", not(test)))]
1343        METRIC_DISPATCHED_MSGS.increment_by(&["unrelated"], 1);
1344
1345        Ok(DispatchResult::Unrelated(in_data))
1346    }
1347
1348    /// Pre-populates the sessions cache with a session slot for benchmarking.
1349    ///
1350    /// Intended for benchmarks that need a session to exist before calling
1351    /// [`SessionManager::dispatch_message`].
1352    ///
1353    /// Requires the `"benchmark"` feature.
1354    #[cfg(feature = "benchmark")]
1355    pub fn pre_populate_session(&self, session_id: SessionId, routing_opts: DestinationRouting) {
1356        let (session_tx, _) =
1357            crossfire::mpsc::bounded_blocking_async::<ApplicationDataIn>(self.cfg.session_forward_capacity);
1358        let slot = SessionSlot {
1359            session_tx,
1360            routing_opts,
1361            abort_handles: Default::default(),
1362            surb_mgmt: Arc::new(BalancerStateValues::default()),
1363            surb_estimator: Default::default(),
1364        };
1365        self.sessions.insert(session_id, slot);
1366    }
1367
1368    /// Like [`pre_populate_session`](SessionManager::pre_populate_session) but also returns the
1369    /// session channel receiver so the caller can spawn a drain task.
1370    ///
1371    /// Requires the `"benchmark"` feature.
1372    #[cfg(feature = "benchmark")]
1373    pub fn pre_populate_session_with_receiver(
1374        &self,
1375        session_id: SessionId,
1376        routing_opts: DestinationRouting,
1377    ) -> crossfire::AsyncRx<crossfire::mpsc::Array<ApplicationDataIn>> {
1378        let (session_tx, session_rx) =
1379            crossfire::mpsc::bounded_blocking_async::<ApplicationDataIn>(self.cfg.session_forward_capacity);
1380        let slot = SessionSlot {
1381            session_tx,
1382            routing_opts,
1383            abort_handles: Default::default(),
1384            surb_mgmt: Arc::new(BalancerStateValues::default()),
1385            surb_estimator: Default::default(),
1386        };
1387        self.sessions.insert(session_id, slot);
1388        session_rx
1389    }
1390
1391    async fn handle_incoming_session_initiation(
1392        &self,
1393        pseudonym: HoprPseudonym,
1394        session_req: StartInitiation<SessionTarget, ByteCapabilities>,
1395    ) -> crate::errors::Result<()> {
1396        trace!(challenge = session_req.challenge, "received session initiation request");
1397
1398        debug!(%pseudonym, "got new session request, searching for a free session slot");
1399
1400        let mut msg_sender = self.msg_sender.get().cloned().ok_or(SessionManagerError::NotStarted)?;
1401
1402        let (new_session_notifier, close_session_notifier) = self
1403            .session_notifiers
1404            .get()
1405            .cloned()
1406            .ok_or(SessionManagerError::NotStarted)?;
1407
1408        // Reply routing uses SURBs only with the pseudonym of this Session's ID
1409        let reply_routing = DestinationRouting::Return(pseudonym.into());
1410
1411        // Use constant application tag for all sessions
1412        self.sessions.run_pending_tasks();
1413
1414        // A repeated initiation for a pseudonym that already has a Session means the
1415        // initiator has lost or abandoned its side of it (it never received our
1416        // SessionEstablished reply, or it reuses its pseudonym on reconnect).
1417        // The pseudonym is known only to the initiator, so the existing Session cannot
1418        // serve anyone else anymore: close it and let this initiation take the slot over.
1419        // Otherwise, re-initiations would keep being rejected with NoSlotsAvailable
1420        // until the stale Session gets evicted by the idle timeout.
1421        if let Some(stale_slot) = self.sessions.remove(&pseudonym) {
1422            self.active_sessions.fetch_sub(1, Ordering::Relaxed);
1423            info!(%pseudonym, "closing stale session superseded by a new initiation with the same pseudonym");
1424            close_session(pseudonym, stale_slot, ClosureReason::Eviction);
1425        }
1426
1427        let session_id = pseudonym;
1428
1429        let (session_tx, session_rx) =
1430            crossfire::mpsc::bounded_blocking_async::<ApplicationDataIn>(self.cfg.session_forward_capacity);
1431        let (session_rx, session_rx_ah) = hopr_utils::runtime::DropAbortable::new(session_rx.into_stream());
1432
1433        let slot = SessionSlot {
1434            session_tx,
1435            routing_opts: reply_routing.clone(),
1436            abort_handles: Default::default(),
1437            surb_mgmt: Default::default(),
1438            surb_estimator: Default::default(),
1439        };
1440        slot.abort_handles.lock().insert(SessionHandles::Ingress, session_rx_ah);
1441
1442        // Insert the slot and obtain a guard. Any failure from here on rolls the slot
1443        // back, otherwise it would block this pseudonym until idle eviction. The atomic
1444        // insert (inside the helper) also prevents a TOCTOU race, so only one concurrent
1445        // request can claim the slot for a given pseudonym.
1446        let Some(mut slot_guard) = self.allocate_session_slot(session_id, slot.clone()) else {
1447            // Either the maximum number of sessions has been reached, or a concurrent
1448            // initiation for the same pseudonym has claimed the slot first.
1449            error!(%pseudonym, "no session slot available");
1450            let reason = StartErrorReason::NoSlotsAvailable;
1451            let data = HoprStartProtocol::SessionError(StartErrorType {
1452                challenge: session_req.challenge,
1453                reason,
1454            });
1455            send_via_msg_sender(&mut msg_sender, reply_routing.clone(), data, "session error message").await?;
1456            return Ok(());
1457        };
1458
1459        debug!(?pseudonym, ?session_req, "assigned a new session");
1460
1461        let closure_notifier = Box::new(move |session_id: SessionId, reason: ClosureReason| {
1462            if let Err(error) = close_session_notifier.try_send((session_id, reason)) {
1463                error!(%session_id, %error, %reason, "failed to notify session closure");
1464            }
1465        });
1466
1467        let session = if !session_req.capabilities.0.contains(Capability::NoRateControl) {
1468            // Because of SURB scarcity, control the egress rate of incoming sessions
1469            let egress_rate_control =
1470                RateController::new(self.cfg.initial_return_session_egress_rate, Duration::from_secs(1));
1471
1472            // The Session request carries a "hint" as additional data telling what
1473            // the Session initiator has configured as its target buffer size in the Balancer.
1474            let target_surb_buffer_size = if session_req.additional_data > 0 {
1475                (session_req.additional_data as u64).min(self.cfg.maximum_surb_buffer_size as u64)
1476            } else {
1477                self.cfg.initial_return_session_egress_rate as u64
1478                    * self
1479                        .cfg
1480                        .minimum_surb_buffer_duration
1481                        .max(MIN_SURB_BUFFER_DURATION)
1482                        .as_secs()
1483            };
1484
1485            let surb_estimator_clone = slot.surb_estimator.clone();
1486            let session = HoprSession::new(
1487                session_id,
1488                reply_routing.clone(),
1489                session_config(&self.cfg, session_req.capabilities.into()),
1490                (
1491                    // Sent packets = SURB consumption estimate
1492                    msg_sender
1493                        .clone()
1494                        .with(move |(routing, data): (DestinationRouting, ApplicationDataOut)| {
1495                            // Each outgoing packet consumes one SURB
1496                            surb_estimator_clone
1497                                .consumed
1498                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1499                            #[cfg(feature = "telemetry")]
1500                            crate::telemetry::record_session_surb_consumed(&session_id, 1);
1501                            futures::future::ok::<_, S::Error>((routing, data))
1502                        })
1503                        .rate_limit_with_controller(&egress_rate_control)
1504                        .buffer((2 * target_surb_buffer_size) as usize),
1505                    // Received packets = SURB retrieval estimate
1506                    session_rx.inspect(move |data| {
1507                        let produced = data.num_surbs_with_msg() as u64;
1508                        // Count the number of SURBs delivered with each incoming packet
1509                        surb_estimator_clone
1510                            .produced
1511                            .fetch_add(produced, std::sync::atomic::Ordering::Relaxed);
1512                        #[cfg(feature = "telemetry")]
1513                        crate::telemetry::record_session_surb_produced(&session_id, produced);
1514                    }),
1515                ),
1516                Some(closure_notifier),
1517            )?;
1518
1519            // The SURB balancer will start intervening by rate-limiting the
1520            // egress of the Session, once the estimated number of SURBs drops below
1521            // the target defined here. Otherwise, the maximum egress is allowed.
1522            let balancer_config = SurbBalancerConfig {
1523                target_surb_buffer_size,
1524                // At maximum egress, the SURB buffer drains in `minimum_surb_buffer_duration` seconds
1525                max_surbs_per_sec: target_surb_buffer_size / self.cfg.minimum_surb_buffer_duration.as_secs(),
1526                // No SURB decay at the Exit, since we know almost exactly how many SURBs
1527                // were received
1528                surb_decay: None,
1529            };
1530
1531            slot.surb_mgmt.update(&balancer_config);
1532
1533            // Spawn the SURB balancer only once we know we have registered the
1534            // abort handle with the pre-allocated Session slot
1535            debug!(%session_id, ?balancer_config ,"spawning exit SURB balancer");
1536            let balancer = SurbBalancer::new(
1537                session_id,
1538                SimpleBalancerController::default(),
1539                slot.surb_estimator.clone(),
1540                SurbControllerWithCorrection(egress_rate_control, 1), // 1 SURB per egress packet
1541                slot.surb_mgmt.clone(),
1542            );
1543
1544            // Assign the SURB balancer and abort handles to the already allocated Session slot
1545            let (_, balancer_abort_handle) = balancer.start_control_loop(self.cfg.balancer_sampling_interval);
1546            slot.abort_handles
1547                .lock()
1548                .insert(SessionHandles::Balancer, balancer_abort_handle);
1549
1550            // Spawn a keep-alive stream notifying about the SURB buffer level towards the Entry
1551            if let Some(period) = self.cfg.surb_balance_notify_period {
1552                let surb_estimator_clone = slot.surb_estimator.clone();
1553                let (ka_controller, ka_abort_handle) = utils::spawn_keep_alive_stream(
1554                    session_id,
1555                    // Sent Keep-Alive packets also contribute to SURB consumption
1556                    msg_sender
1557                        .clone()
1558                        .with(move |(routing, data): (DestinationRouting, ApplicationDataOut)| {
1559                            // Each sent keepalive consumes 1 SURB
1560                            surb_estimator_clone
1561                                .consumed
1562                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1563                            #[cfg(feature = "telemetry")]
1564                            crate::telemetry::record_session_surb_consumed(&session_id, 1);
1565                            futures::future::ok::<_, S::Error>((routing, data))
1566                        }),
1567                    slot.routing_opts.clone(),
1568                    SurbNotificationMode::Level(slot.surb_estimator.clone()),
1569                    slot.surb_mgmt.clone(),
1570                );
1571
1572                // Start keepalive stream towards the Entry with a predefined period
1573                hopr_utils::runtime::prelude::spawn(async move {
1574                    // Delay the stream execution by one period
1575                    hopr_utils::runtime::prelude::sleep(period).await;
1576                    ka_controller.set_rate_per_unit(1, period);
1577                });
1578
1579                slot.abort_handles
1580                    .lock()
1581                    .insert(SessionHandles::KeepAlive, ka_abort_handle);
1582
1583                debug!(%session_id, ?period, "started SURB level-notifying keep-alive stream");
1584            }
1585
1586            session
1587        } else {
1588            HoprSession::new(
1589                session_id,
1590                reply_routing.clone(),
1591                session_config(&self.cfg, session_req.capabilities.into()),
1592                (msg_sender.clone(), session_rx),
1593                Some(closure_notifier),
1594            )?
1595        };
1596
1597        // Extract useful information about the session from the Start protocol message
1598        let incoming_session = IncomingSession {
1599            session,
1600            target: session_req.target,
1601        };
1602
1603        // Notify that a new incoming session has been created. Lock the sink and send
1604        // directly into it, so no extra forwarding task between channels is needed.
1605        match async {
1606            let mut guard = new_session_notifier.lock().await;
1607            guard.send(incoming_session).await
1608        }
1609        .timeout(futures_time::time::Duration::from(EXTERNAL_SEND_TIMEOUT))
1610        .await
1611        {
1612            Err(_) => {
1613                error!(%session_id, "timeout to notify about new incoming session");
1614                return Err(TransportSessionError::Timeout);
1615            }
1616            Ok(Err(error)) => {
1617                error!(%session_id, %error, "failed to notify about new incoming session");
1618                return Err(SessionManagerError::other(error).into());
1619            }
1620            _ => {}
1621        };
1622
1623        trace!(?session_id, "session notification sent");
1624
1625        // Notify the sender that the session has been established.
1626        // Set our peer ID in the session ID sent back to them.
1627        let data = HoprStartProtocol::SessionEstablished(StartEstablished {
1628            orig_challenge: session_req.challenge,
1629            session_id,
1630        });
1631
1632        send_via_msg_sender(
1633            &mut msg_sender,
1634            reply_routing.clone(),
1635            data,
1636            "session establishment message",
1637        )
1638        .await?;
1639
1640        #[cfg(feature = "telemetry")]
1641        initialize_session_telemetry(
1642            session_id,
1643            &self.cfg,
1644            session_req.capabilities.0,
1645            Some(&slot.surb_estimator),
1646            Some(&slot.surb_mgmt),
1647        );
1648
1649        info!(%session_id, "new session established");
1650
1651        #[cfg(all(feature = "telemetry", not(test)))]
1652        METRIC_NUM_ESTABLISHED_SESSIONS.increment();
1653
1654        slot_guard.commit();
1655        Ok(())
1656    }
1657
1658    async fn handle_session_established(&self, est: StartEstablished<SessionId>) -> crate::errors::Result<()> {
1659        trace!(
1660            session_id = ?est.session_id,
1661            "received session establishment confirmation"
1662        );
1663        let challenge = est.orig_challenge;
1664        let session_id = est.session_id;
1665        if let Some(tx_est) = self.session_initiations.remove(&est.orig_challenge) {
1666            if let Err(error) = tx_est.try_send(Ok(est)) {
1667                error!(%challenge, %session_id, %error, "failed to send session establishment confirmation");
1668                return Err(SessionManagerError::other(error).into());
1669            }
1670            debug!(?session_id, challenge, "session establishment complete");
1671        } else {
1672            error!(%session_id, challenge, "unknown session establishment attempt or expired");
1673        }
1674        Ok(())
1675    }
1676
1677    async fn handle_session_error(&self, error_type: StartErrorType) -> crate::errors::Result<()> {
1678        trace!(
1679            challenge = error_type.challenge,
1680            error = ?error_type.reason,
1681            "failed to initialize a session",
1682        );
1683        // Currently, we do not distinguish between individual error types
1684        // and just discard the initiation attempt and pass on the error.
1685        if let Some(tx_est) = self.session_initiations.remove(&error_type.challenge) {
1686            if let Err(error) = tx_est.try_send(Err(error_type)) {
1687                error!(%error, ?error_type, "could not send session error message");
1688                return Err(SessionManagerError::other(error).into());
1689            }
1690            error!(
1691                challenge = error_type.challenge,
1692                ?error_type,
1693                "session establishment error received"
1694            );
1695        } else {
1696            error!(
1697                challenge = error_type.challenge,
1698                ?error_type,
1699                "session establishment attempt expired before error could be delivered"
1700            );
1701        }
1702
1703        #[cfg(all(feature = "telemetry", not(test)))]
1704        METRIC_RECEIVED_SESSION_ERRS.increment(&[&error_type.reason.to_string()]);
1705
1706        Ok(())
1707    }
1708
1709    async fn handle_keep_alive(&self, msg: KeepAliveMessage<SessionId>) -> crate::errors::Result<()> {
1710        let session_id = msg.session_id;
1711        if let Some(session_slot) = self.sessions.get(&session_id) {
1712            trace!(?session_id, "received keep-alive message");
1713            match &session_slot.routing_opts {
1714                // Session is outgoing - keep-alive was received from the Exit
1715                DestinationRouting::Forward { .. } => {
1716                    if msg.flags.contains(KeepAliveFlag::BalancerState)
1717                        && !session_slot.surb_mgmt.is_disabled()
1718                        && session_slot.surb_mgmt.buffer_level() != msg.additional_data
1719                    {
1720                        // Update the buffer level as sent to us from the Exit
1721                        session_slot
1722                            .surb_mgmt
1723                            .buffer_level
1724                            .store(msg.additional_data, std::sync::atomic::Ordering::Relaxed);
1725                        debug!(%session_id, surb_level = msg.additional_data, "keep-alive updated SURB buffer size from the Exit");
1726                    }
1727
1728                    // Increase the number of consumed SURBs in the estimator
1729                    session_slot
1730                        .surb_estimator
1731                        .consumed
1732                        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1733                    #[cfg(feature = "telemetry")]
1734                    crate::telemetry::record_session_surb_consumed(&session_id, 1);
1735                }
1736                // Session is incoming - keep-alive was received from the Entry
1737                DestinationRouting::Return(_) => {
1738                    // Allow updating SURB balancer target based on the received Keep-Alive message
1739                    if msg.flags.contains(KeepAliveFlag::BalancerTarget)
1740                        && msg.additional_data > 0
1741                        && !session_slot.surb_mgmt.is_disabled()
1742                        && session_slot.surb_mgmt.controller_bounds().target() != msg.additional_data
1743                    {
1744                        // Update the target buffer size as sent to us from the Entry
1745                        session_slot
1746                            .surb_mgmt
1747                            .target_surb_buffer_size
1748                            .store(msg.additional_data, std::sync::atomic::Ordering::Relaxed);
1749                        // Update maximum SURBs per second based on the new target
1750                        session_slot.surb_mgmt.max_surbs_per_sec.store(
1751                            msg.additional_data / self.cfg.minimum_surb_buffer_duration.as_secs(),
1752                            std::sync::atomic::Ordering::Relaxed,
1753                        );
1754                        debug!(%session_id, target_surb_buffer_size = msg.additional_data, "keep-alive updated SURB balancer target buffer size from the Entry");
1755                    }
1756
1757                    // Increase the number of received SURBs in the estimator.
1758                    // Typically, 2 SURBs per Keep-Alive message
1759                    let produced = KeepAliveMessage::<SessionId>::MIN_SURBS_PER_MESSAGE as u64;
1760                    session_slot
1761                        .surb_estimator
1762                        .produced
1763                        .fetch_add(produced, std::sync::atomic::Ordering::Relaxed);
1764                    #[cfg(feature = "telemetry")]
1765                    crate::telemetry::record_session_surb_produced(&session_id, produced);
1766                }
1767            }
1768        } else {
1769            debug!(%session_id, "received keep-alive request for an unknown session");
1770        }
1771        Ok(())
1772    }
1773}
1774
1775#[cfg(test)]
1776mod tests {
1777    use anyhow::{Context, anyhow};
1778    use futures::{AsyncWriteExt, channel::mpsc::UnboundedSender, future::BoxFuture, pin_mut};
1779    use hopr_api::types::{
1780        crypto::{keypairs::ChainKeypair, prelude::Keypair},
1781        crypto_random::Randomizable,
1782        internal::routing::SurbMatcher,
1783        primitive::prelude::Address,
1784    };
1785    use hopr_protocol_start::{StartProtocol, StartProtocolDiscriminants};
1786    use hopr_utils::network_types::prelude::SealedHost;
1787    use moka::future::FutureExt;
1788    use tokio::time::timeout;
1789
1790    use super::*;
1791    use crate::{Capabilities, balancer::SurbBalancerConfig, types::SessionTarget};
1792
1793    #[test]
1794    fn session_config_forwards_max_buffered_segments() {
1795        assert_eq!(
1796            SessionManagerConfig::default().max_buffered_segments,
1797            0,
1798            "default must leave the transport unbuffered"
1799        );
1800
1801        for segments in [0, 64] {
1802            let cfg = SessionManagerConfig {
1803                max_buffered_segments: segments,
1804                ..Default::default()
1805            };
1806            assert_eq!(
1807                session_config(&cfg, Capabilities::empty()).max_buffered_segments,
1808                segments
1809            );
1810        }
1811    }
1812
1813    #[async_trait::async_trait]
1814    trait SendMsg {
1815        async fn send_message(
1816            &self,
1817            routing: DestinationRouting,
1818            data: ApplicationDataOut,
1819        ) -> crate::errors::Result<()>;
1820    }
1821
1822    mockall::mock! {
1823        MsgSender {}
1824        impl SendMsg for MsgSender {
1825            fn send_message<'a, 'b>(&'a self, routing: DestinationRouting, data: ApplicationDataOut)
1826            -> BoxFuture<'b, crate::errors::Result<()>> where 'a: 'b, Self: Sync + 'b;
1827        }
1828    }
1829
1830    fn mock_packet_planning(
1831        sender: MockMsgSender,
1832    ) -> (
1833        UnboundedSender<(DestinationRouting, ApplicationDataOut)>,
1834        tokio::task::JoinHandle<()>,
1835    ) {
1836        let (tx, rx) = futures::channel::mpsc::unbounded();
1837        let handle = tokio::task::spawn(async move {
1838            pin_mut!(rx);
1839            while let Some((routing, data)) = rx.next().await {
1840                sender
1841                    .send_message(routing, data)
1842                    .await
1843                    .expect("send message must not fail in mock");
1844            }
1845        });
1846        (tx, handle)
1847    }
1848
1849    fn msg_type(data: &ApplicationDataOut, expected: StartProtocolDiscriminants) -> bool {
1850        HoprStartProtocol::decode(data.data.application_tag, &data.data.plain_text)
1851            .map(|d| StartProtocolDiscriminants::from(d) == expected)
1852            .unwrap_or(false)
1853    }
1854
1855    fn start_msg_match(data: &ApplicationDataOut, msg: impl Fn(HoprStartProtocol) -> bool) -> bool {
1856        HoprStartProtocol::decode(data.data.application_tag, &data.data.plain_text)
1857            .map(msg)
1858            .unwrap_or(false)
1859    }
1860
1861    /// Waits (bounded) until the manager reports no active sessions.
1862    ///
1863    /// The session-slot rollback runs on a spawned task, so its effect is observed
1864    /// asynchronously; this polls [`SessionManager::active_sessions`] until it drains.
1865    async fn wait_for_no_active_sessions(
1866        mgr: &SessionManager<UnboundedSender<(DestinationRouting, ApplicationDataOut)>>,
1867    ) -> bool {
1868        for _ in 0..50 {
1869            if mgr.active_sessions().is_empty() {
1870                return true;
1871            }
1872            tokio::time::sleep(Duration::from_millis(20)).await;
1873        }
1874        mgr.active_sessions().is_empty()
1875    }
1876
1877    #[test_log::test(tokio::test)]
1878    async fn session_manager_should_follow_start_protocol_to_establish_new_session_and_close_it() -> anyhow::Result<()>
1879    {
1880        let alice_pseudonym = HoprPseudonym::random();
1881        let bob_peer: Address = (&ChainKeypair::random()).into();
1882
1883        let alice_mgr = SessionManager::new(Default::default());
1884        let bob_mgr = SessionManager::new(Default::default());
1885
1886        let mut sequence = mockall::Sequence::new();
1887        let mut alice_transport = MockMsgSender::new();
1888        let mut bob_transport = MockMsgSender::new();
1889
1890        // Alice sends the StartSession message
1891        let bob_mgr_clone = bob_mgr.clone();
1892        alice_transport
1893            .expect_send_message()
1894            .once()
1895            .in_sequence(&mut sequence)
1896            .withf(move |peer, data| {
1897                info!("alice sends {}", data.data.application_tag);
1898                msg_type(data, StartProtocolDiscriminants::StartSession)
1899                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
1900            })
1901            .returning(move |_, data| {
1902                let bob_mgr_clone = bob_mgr_clone.clone();
1903                Box::pin(async move {
1904                    bob_mgr_clone
1905                        .dispatch_message(
1906                            alice_pseudonym,
1907                            ApplicationDataIn {
1908                                data: data.data,
1909                                packet_info: Default::default(),
1910                            },
1911                        )
1912                        ?;
1913                    Ok(())
1914                })
1915            });
1916
1917        // Bob sends the SessionEstablished message
1918        let alice_mgr_clone = alice_mgr.clone();
1919        bob_transport
1920            .expect_send_message()
1921            .once()
1922            .in_sequence(&mut sequence)
1923            .withf(move |peer, data| {
1924                info!("bob sends {}", data.data.application_tag);
1925                msg_type(data, StartProtocolDiscriminants::SessionEstablished)
1926                    && matches!(peer, DestinationRouting::Return(SurbMatcher::Pseudonym(p)) if p == &alice_pseudonym)
1927            })
1928            .returning(move |_, data| {
1929                let alice_mgr_clone = alice_mgr_clone.clone();
1930
1931                Box::pin(async move {
1932                    alice_mgr_clone.dispatch_message(
1933                        alice_pseudonym,
1934                        ApplicationDataIn {
1935                            data: data.data,
1936                            packet_info: Default::default(),
1937                        },
1938                    )?;
1939                    Ok(())
1940                })
1941            });
1942
1943        // Alice sends the terminating segment to close the Session
1944        let bob_mgr_clone = bob_mgr.clone();
1945        alice_transport
1946            .expect_send_message()
1947            .once()
1948            .in_sequence(&mut sequence)
1949            .withf(move |peer, data| {
1950                hopr_protocol_session::types::SessionMessage::<{ ApplicationData::PAYLOAD_SIZE }>::try_from(
1951                    data.data.plain_text.as_ref(),
1952                )
1953                .expect("must be a session message")
1954                .try_as_segment()
1955                .expect("must be a segment")
1956                .is_terminating()
1957                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
1958            })
1959            .returning(move |_, data| {
1960                let bob_mgr_clone = bob_mgr_clone.clone();
1961                Box::pin(async move {
1962                    bob_mgr_clone
1963                        .dispatch_message(
1964                            alice_pseudonym,
1965                            ApplicationDataIn {
1966                                data: data.data,
1967                                packet_info: Default::default(),
1968                            },
1969                        )
1970                        ?;
1971                    Ok(())
1972                })
1973            });
1974
1975        let mut ahs = Vec::new();
1976
1977        // Start Alice
1978        let (new_session_tx_alice, _) = futures::channel::mpsc::channel(1024);
1979        let (alice_sender, alice_handle) = mock_packet_planning(alice_transport);
1980        ahs.extend(alice_mgr.start(alice_sender.clone(), new_session_tx_alice)?);
1981        assert!(alice_mgr.is_started());
1982
1983        // Start Bob
1984        let (new_session_tx_bob, new_session_rx_bob) = futures::channel::mpsc::channel(1024);
1985        let (bob_sender, bob_handle) = mock_packet_planning(bob_transport);
1986        ahs.extend(bob_mgr.start(bob_sender.clone(), new_session_tx_bob)?);
1987        assert!(bob_mgr.is_started());
1988
1989        let target = SealedHost::Plain("127.0.0.1:80".parse()?);
1990
1991        pin_mut!(new_session_rx_bob);
1992        let (alice_session, bob_session) = timeout(
1993            Duration::from_secs(2),
1994            futures::future::join(
1995                alice_mgr.new_session(
1996                    bob_peer,
1997                    SessionTarget::TcpStream(target.clone()),
1998                    SessionClientConfig {
1999                        pseudonym: alice_pseudonym.into(),
2000                        capabilities: Capability::NoRateControl | Capability::Segmentation,
2001                        surb_management: None,
2002                        ..Default::default()
2003                    },
2004                ),
2005                new_session_rx_bob.next(),
2006            ),
2007        )
2008        .await?;
2009
2010        let mut alice_session = alice_session?;
2011        let bob_session = bob_session.ok_or(anyhow!("bob must get an incoming session"))?;
2012
2013        assert_eq!(
2014            alice_session.config().capabilities,
2015            Capability::Segmentation | Capability::NoRateControl
2016        );
2017        assert_eq!(
2018            alice_session.config().capabilities,
2019            bob_session.session.config().capabilities
2020        );
2021        assert!(matches!(bob_session.target, SessionTarget::TcpStream(host) if host == target));
2022
2023        assert_eq!(vec![*alice_session.id()], alice_mgr.active_sessions());
2024        assert_eq!(None, alice_mgr.get_surb_balancer_config(alice_session.id())?);
2025        assert!(
2026            alice_mgr
2027                .update_surb_balancer_config(alice_session.id(), SurbBalancerConfig::default())
2028                .is_err()
2029        );
2030
2031        assert_eq!(vec![*bob_session.session.id()], bob_mgr.active_sessions());
2032        assert_eq!(None, bob_mgr.get_surb_balancer_config(bob_session.session.id())?);
2033        assert!(
2034            bob_mgr
2035                .update_surb_balancer_config(bob_session.session.id(), SurbBalancerConfig::default())
2036                .is_err()
2037        );
2038
2039        tokio::time::sleep(Duration::from_millis(100)).await;
2040        alice_session.close().await?;
2041
2042        tokio::time::sleep(Duration::from_millis(100)).await;
2043
2044        assert!(matches!(
2045            alice_mgr.ping_session(alice_session.id()).await,
2046            Err(TransportSessionError::Manager(SessionManagerError::NonExistingSession))
2047        ));
2048
2049        futures::stream::iter(ahs)
2050            .for_each(|ah| async move { ah.abort() })
2051            .await;
2052
2053        // Cleanup: close senders and await handles
2054        alice_sender.close_channel();
2055        bob_sender.close_channel();
2056        let _ = alice_handle.await;
2057        let _ = bob_handle.await;
2058
2059        Ok(())
2060    }
2061
2062    #[test_log::test(tokio::test)]
2063    async fn session_manager_should_close_idle_session_automatically() -> anyhow::Result<()> {
2064        let alice_pseudonym = HoprPseudonym::random();
2065        let bob_peer: Address = (&ChainKeypair::random()).into();
2066
2067        let cfg = SessionManagerConfig {
2068            idle_timeout: Duration::from_millis(200),
2069            ..Default::default()
2070        };
2071
2072        let alice_mgr = SessionManager::new(cfg);
2073        let bob_mgr = SessionManager::new(Default::default());
2074
2075        let mut sequence = mockall::Sequence::new();
2076        let mut alice_transport = MockMsgSender::new();
2077        let mut bob_transport = MockMsgSender::new();
2078
2079        // Alice sends the StartSession message
2080        let bob_mgr_clone = bob_mgr.clone();
2081        alice_transport
2082            .expect_send_message()
2083            .once()
2084            .in_sequence(&mut sequence)
2085            .withf(move |peer, data| {
2086                msg_type(data, StartProtocolDiscriminants::StartSession)
2087                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
2088            })
2089            .returning(move |_, data| {
2090                let bob_mgr_clone = bob_mgr_clone.clone();
2091                Box::pin(async move {
2092                    bob_mgr_clone
2093                        .dispatch_message(
2094                            alice_pseudonym,
2095                            ApplicationDataIn {
2096                                data: data.data,
2097                                packet_info: Default::default(),
2098                            },
2099                        )
2100                        ?;
2101                    Ok(())
2102                })
2103            });
2104
2105        // Bob sends the SessionEstablished message
2106        let alice_mgr_clone = alice_mgr.clone();
2107        bob_transport
2108            .expect_send_message()
2109            .once()
2110            .in_sequence(&mut sequence)
2111            .withf(move |peer, data| {
2112                msg_type(data, StartProtocolDiscriminants::SessionEstablished)
2113                    && matches!(peer, DestinationRouting::Return(SurbMatcher::Pseudonym(p)) if p == &alice_pseudonym)
2114            })
2115            .returning(move |_, data| {
2116                let alice_mgr_clone = alice_mgr_clone.clone();
2117
2118                Box::pin(async move {
2119                    alice_mgr_clone.dispatch_message(
2120                        alice_pseudonym,
2121                        ApplicationDataIn {
2122                            data: data.data,
2123                            packet_info: Default::default(),
2124                        },
2125                    )?;
2126                    Ok(())
2127                })
2128            });
2129
2130        let mut ahs = Vec::new();
2131
2132        // Start Alice
2133        let (new_session_tx_alice, _) = futures::channel::mpsc::channel(1024);
2134        let (alice_sender, alice_handle) = mock_packet_planning(alice_transport);
2135        ahs.extend(alice_mgr.start(alice_sender.clone(), new_session_tx_alice)?);
2136
2137        // Start Bob
2138        let (new_session_tx_bob, new_session_rx_bob) = futures::channel::mpsc::channel(1024);
2139        let (bob_sender, bob_handle) = mock_packet_planning(bob_transport);
2140        ahs.extend(bob_mgr.start(bob_sender.clone(), new_session_tx_bob)?);
2141        assert!(bob_mgr.is_started());
2142
2143        let target = SealedHost::Plain("127.0.0.1:80".parse()?);
2144
2145        pin_mut!(new_session_rx_bob);
2146        let (alice_session, bob_session) = timeout(
2147            Duration::from_secs(2),
2148            futures::future::join(
2149                alice_mgr.new_session(
2150                    bob_peer,
2151                    SessionTarget::TcpStream(target.clone()),
2152                    SessionClientConfig {
2153                        pseudonym: alice_pseudonym.into(),
2154                        capabilities: Capability::NoRateControl | Capability::Segmentation,
2155                        surb_management: None,
2156                        ..Default::default()
2157                    },
2158                ),
2159                new_session_rx_bob.next(),
2160            ),
2161        )
2162        .await?;
2163
2164        let alice_session = alice_session?;
2165        let bob_session = bob_session.ok_or(anyhow!("bob must get an incoming session"))?;
2166
2167        assert_eq!(
2168            alice_session.config().capabilities,
2169            Capability::Segmentation | Capability::NoRateControl,
2170        );
2171        assert_eq!(
2172            alice_session.config().capabilities,
2173            bob_session.session.config().capabilities
2174        );
2175        assert!(matches!(bob_session.target, SessionTarget::TcpStream(host) if host == target));
2176
2177        // Let the session timeout at Alice
2178        tokio::time::sleep(Duration::from_millis(300)).await;
2179
2180        assert!(matches!(
2181            alice_mgr.ping_session(alice_session.id()).await,
2182            Err(TransportSessionError::Manager(SessionManagerError::NonExistingSession))
2183        ));
2184
2185        futures::stream::iter(ahs)
2186            .for_each(|ah| async move { ah.abort() })
2187            .await;
2188
2189        // Cleanup: close senders and await handles
2190        alice_sender.close_channel();
2191        bob_sender.close_channel();
2192        let _ = alice_handle.await;
2193        let _ = bob_handle.await;
2194
2195        Ok(())
2196    }
2197
2198    #[test_log::test(tokio::test)]
2199    async fn session_manager_should_update_surb_balancer_config() -> anyhow::Result<()> {
2200        let alice_pseudonym = HoprPseudonym::random();
2201        let session_id = alice_pseudonym;
2202        let balancer_cfg = SurbBalancerConfig {
2203            target_surb_buffer_size: 1000,
2204            max_surbs_per_sec: 100,
2205            ..Default::default()
2206        };
2207
2208        let alice_mgr =
2209            SessionManager::<UnboundedSender<(DestinationRouting, ApplicationDataOut)>>::new(Default::default());
2210
2211        let (dummy_tx, _) = crossfire::mpsc::bounded_blocking_async::<ApplicationDataIn>(SESSION_FORWARD_CAPACITY);
2212        alice_mgr.sessions.insert(
2213            session_id,
2214            SessionSlot {
2215                session_tx: dummy_tx,
2216                routing_opts: DestinationRouting::Return(SurbMatcher::Pseudonym(alice_pseudonym)),
2217                abort_handles: Default::default(),
2218                surb_mgmt: Arc::new(BalancerStateValues::from(balancer_cfg)),
2219                surb_estimator: Default::default(),
2220            },
2221        );
2222
2223        let actual_cfg = alice_mgr
2224            .get_surb_balancer_config(&session_id)?
2225            .ok_or(anyhow!("session must have a surb balancer config"))?;
2226        assert_eq!(actual_cfg, balancer_cfg);
2227
2228        let new_cfg = SurbBalancerConfig {
2229            target_surb_buffer_size: 2000,
2230            max_surbs_per_sec: 200,
2231            ..Default::default()
2232        };
2233        alice_mgr.update_surb_balancer_config(&session_id, new_cfg)?;
2234
2235        let actual_cfg = alice_mgr
2236            .get_surb_balancer_config(&session_id)?
2237            .ok_or(anyhow!("session must have a surb balancer config"))?;
2238        assert_eq!(actual_cfg, new_cfg);
2239
2240        Ok(())
2241    }
2242
2243    #[test_log::test(tokio::test)]
2244    async fn session_manager_should_not_allow_loopback_sessions() -> anyhow::Result<()> {
2245        let alice_pseudonym = HoprPseudonym::random();
2246        let bob_peer: Address = (&ChainKeypair::random()).into();
2247
2248        let alice_mgr = SessionManager::new(Default::default());
2249
2250        let mut sequence = mockall::Sequence::new();
2251        let mut alice_transport = MockMsgSender::new();
2252
2253        // Alice sends the StartSession message
2254        let alice_mgr_clone = alice_mgr.clone();
2255        alice_transport
2256            .expect_send_message()
2257            .once()
2258            .in_sequence(&mut sequence)
2259            .withf(move |peer, data| {
2260                msg_type(data, StartProtocolDiscriminants::StartSession)
2261                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
2262            })
2263            .returning(move |_, data| {
2264                // But the message is again processed by Alice due to Loopback
2265                let alice_mgr_clone = alice_mgr_clone.clone();
2266                Box::pin(async move {
2267                    alice_mgr_clone
2268                        .dispatch_message(
2269                            alice_pseudonym,
2270                            ApplicationDataIn {
2271                                data: data.data,
2272                                packet_info: Default::default(),
2273                            },
2274                        )
2275                        ?;
2276                    Ok(())
2277                })
2278            });
2279
2280        // Alice sends the SessionEstablished message (as Bob)
2281        let alice_mgr_clone = alice_mgr.clone();
2282        alice_transport
2283            .expect_send_message()
2284            .once()
2285            .in_sequence(&mut sequence)
2286            .withf(move |peer, data| {
2287                msg_type(data, StartProtocolDiscriminants::SessionEstablished)
2288                    && matches!(peer, DestinationRouting::Return(SurbMatcher::Pseudonym(p)) if p == &alice_pseudonym)
2289            })
2290            .returning(move |_, data| {
2291                let alice_mgr_clone = alice_mgr_clone.clone();
2292
2293                Box::pin(async move {
2294                    alice_mgr_clone.dispatch_message(
2295                        alice_pseudonym,
2296                        ApplicationDataIn {
2297                            data: data.data,
2298                            packet_info: Default::default(),
2299                        },
2300                    )?;
2301                    Ok(())
2302                })
2303            });
2304
2305        // Start Alice
2306        let (new_session_tx_alice, new_session_rx_alice) = futures::channel::mpsc::channel(1024);
2307        let (alice_sender, alice_handle) = mock_packet_planning(alice_transport);
2308        alice_mgr.start(alice_sender.clone(), new_session_tx_alice)?;
2309        assert!(alice_mgr.is_started());
2310
2311        let alice_session = alice_mgr
2312            .new_session(
2313                bob_peer,
2314                SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
2315                SessionClientConfig {
2316                    capabilities: None.into(),
2317                    pseudonym: alice_pseudonym.into(),
2318                    surb_management: None,
2319                    ..Default::default()
2320                },
2321            )
2322            .await;
2323
2324        println!("{alice_session:?}");
2325        assert!(matches!(
2326            alice_session,
2327            Err(TransportSessionError::Manager(SessionManagerError::Loopback))
2328        ));
2329
2330        drop(new_session_rx_alice);
2331
2332        // Cleanup: close sender and await handle
2333        alice_sender.close_channel();
2334        let _ = alice_handle.await;
2335
2336        Ok(())
2337    }
2338
2339    #[test_log::test(tokio::test)]
2340    async fn session_manager_should_timeout_new_session_attempt_when_no_response() -> anyhow::Result<()> {
2341        let bob_peer: Address = (&ChainKeypair::random()).into();
2342
2343        let cfg = SessionManagerConfig {
2344            initiation_timeout_base: Duration::from_millis(100),
2345            ..Default::default()
2346        };
2347
2348        let alice_mgr = SessionManager::new(cfg);
2349        let bob_mgr = SessionManager::new(Default::default());
2350
2351        let mut sequence = mockall::Sequence::new();
2352        let mut alice_transport = MockMsgSender::new();
2353        let bob_transport = MockMsgSender::new();
2354
2355        // Alice sends the StartSession message, but Bob does not handle it
2356        alice_transport
2357            .expect_send_message()
2358            .once()
2359            .in_sequence(&mut sequence)
2360            .withf(move |peer, data| {
2361                msg_type(data, StartProtocolDiscriminants::StartSession)
2362                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
2363            })
2364            .returning(|_, _| Box::pin(async { Ok(()) }));
2365
2366        // Start Alice
2367        let (new_session_tx_alice, _) = futures::channel::mpsc::channel(1024);
2368        let (alice_sender, _alice_handle) = mock_packet_planning(alice_transport);
2369        alice_mgr.start(alice_sender.clone(), new_session_tx_alice)?;
2370        assert!(alice_mgr.is_started());
2371
2372        // Start Bob
2373        let (new_session_tx_bob, _) = futures::channel::mpsc::channel(1024);
2374        let (bob_sender, _bob_handle) = mock_packet_planning(bob_transport);
2375        bob_mgr.start(bob_sender.clone(), new_session_tx_bob)?;
2376        assert!(bob_mgr.is_started());
2377
2378        let result = alice_mgr
2379            .new_session(
2380                bob_peer,
2381                SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
2382                SessionClientConfig {
2383                    capabilities: None.into(),
2384                    pseudonym: None,
2385                    surb_management: None,
2386                    ..Default::default()
2387                },
2388            )
2389            .await;
2390
2391        assert!(matches!(result, Err(TransportSessionError::Timeout)));
2392
2393        Ok(())
2394    }
2395
2396    #[cfg(feature = "telemetry")]
2397    #[test_log::test(tokio::test)]
2398    async fn failed_incoming_session_establishment_does_not_register_telemetry() -> anyhow::Result<()> {
2399        let mgr = SessionManager::new(Default::default());
2400
2401        let transport = MockMsgSender::new();
2402        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2403        drop(new_session_rx);
2404        let (sender, _handle) = mock_packet_planning(transport);
2405        mgr.start(sender.clone(), new_session_tx)?;
2406        assert!(mgr.is_started());
2407
2408        let pseudonym = HoprPseudonym::random();
2409        let result = mgr
2410            .handle_incoming_session_initiation(
2411                pseudonym,
2412                StartInitiation {
2413                    challenge: MIN_CHALLENGE,
2414                    target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
2415                    capabilities: ByteCapabilities(Capabilities::empty()),
2416                    additional_data: 0,
2417                },
2418            )
2419            .await;
2420
2421        assert!(result.is_err());
2422
2423        // The slot inserted before the failure must be rolled back, so it neither
2424        // counts towards `maximum_sessions` nor registers any telemetry.
2425        assert!(
2426            wait_for_no_active_sessions(&mgr).await,
2427            "the partially established session slot was not rolled back"
2428        );
2429
2430        // Cleanup: close sender and await handle
2431        sender.close_channel();
2432        let _ = _handle.await;
2433
2434        Ok(())
2435    }
2436
2437    #[test_log::test(tokio::test)]
2438    async fn session_manager_should_roll_back_slot_when_incoming_session_setup_fails() -> anyhow::Result<()> {
2439        let mgr = SessionManager::new(Default::default());
2440
2441        // Drop the receiver so that notifying about the new incoming session fails
2442        // *after* the session slot has already been inserted into the cache.
2443        let transport = MockMsgSender::new();
2444        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2445        drop(new_session_rx);
2446        let (sender, handle) = mock_packet_planning(transport);
2447        mgr.start(sender.clone(), new_session_tx)?;
2448        assert!(mgr.is_started());
2449
2450        let pseudonym = HoprPseudonym::random();
2451
2452        // The setup fails after the slot is inserted (notifying about the new
2453        // incoming session errors out because the receiver is gone), so the slot
2454        // must be rolled back instead of lingering until idle eviction.
2455        let result = mgr
2456            .handle_incoming_session_initiation(
2457                pseudonym,
2458                StartInitiation {
2459                    challenge: MIN_CHALLENGE,
2460                    target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
2461                    capabilities: ByteCapabilities(Capabilities::empty()),
2462                    additional_data: 0,
2463                },
2464            )
2465            .await;
2466        assert!(result.is_err());
2467
2468        // An empty active-session set proves the slot was removed and, since
2469        // sessions are keyed by pseudonym, that the pseudonym is free again.
2470        assert!(
2471            wait_for_no_active_sessions(&mgr).await,
2472            "the partially established session slot was not rolled back"
2473        );
2474
2475        // Cleanup
2476        sender.close_channel();
2477        let _ = handle.await;
2478
2479        Ok(())
2480    }
2481
2482    #[test_log::test(tokio::test)]
2483    async fn session_manager_should_send_keep_alives_via_surb_balancer() -> anyhow::Result<()> {
2484        let alice_pseudonym = HoprPseudonym::random();
2485        let bob_peer: Address = (&ChainKeypair::random()).into();
2486
2487        let bob_cfg = SessionManagerConfig {
2488            surb_balance_notify_period: Some(Duration::from_millis(500)),
2489            ..Default::default()
2490        };
2491        let alice_mgr = SessionManager::new(Default::default());
2492        let bob_mgr = SessionManager::new(bob_cfg.clone());
2493
2494        let mut alice_transport = MockMsgSender::new();
2495        let mut bob_transport = MockMsgSender::new();
2496
2497        // Alice sends the StartSession message
2498        let mut open_sequence = mockall::Sequence::new();
2499        let bob_mgr_clone = bob_mgr.clone();
2500        alice_transport
2501            .expect_send_message()
2502            .once()
2503            .in_sequence(&mut open_sequence)
2504            .withf(move |peer, data| {
2505                msg_type(data, StartProtocolDiscriminants::StartSession)
2506                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
2507            })
2508            .returning(move |_, data| {
2509                let bob_mgr_clone = bob_mgr_clone.clone();
2510                Box::pin(async move {
2511                    bob_mgr_clone
2512                        .dispatch_message(
2513                            alice_pseudonym,
2514                            ApplicationDataIn {
2515                                data: data.data,
2516                                packet_info: Default::default(),
2517                            },
2518                        )
2519                        ?;
2520                    Ok(())
2521                })
2522            });
2523
2524        // Bob sends the SessionEstablished message
2525        let alice_mgr_clone = alice_mgr.clone();
2526        bob_transport
2527            .expect_send_message()
2528            .once()
2529            .in_sequence(&mut open_sequence)
2530            .withf(move |peer, data| {
2531                msg_type(data, StartProtocolDiscriminants::SessionEstablished)
2532                    && matches!(peer, DestinationRouting::Return(SurbMatcher::Pseudonym(p)) if p == &alice_pseudonym)
2533            })
2534            .returning(move |_, data| {
2535                let alice_mgr_clone = alice_mgr_clone.clone();
2536                Box::pin(async move {
2537                    alice_mgr_clone.dispatch_message(
2538                        alice_pseudonym,
2539                        ApplicationDataIn {
2540                            data: data.data,
2541                            packet_info: Default::default(),
2542                        },
2543                    )?;
2544                    Ok(())
2545                })
2546            });
2547
2548        const INITIAL_BALANCER_TARGET: u64 = 10;
2549
2550        // Alice sends the KeepAlive messages reporting the initial balancer target
2551        let bob_mgr_clone = bob_mgr.clone();
2552        alice_transport
2553            .expect_send_message()
2554            .times(5..)
2555            //.in_sequence(&mut sequence)
2556            .withf(move |peer, data| {
2557                start_msg_match(data, |msg| matches!(msg, StartProtocol::KeepAlive(ka) if ka.flags.contains(KeepAliveFlag::BalancerTarget) && ka.additional_data == INITIAL_BALANCER_TARGET))
2558                //msg_type(data, StartProtocolDiscriminants::KeepAlive)
2559                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
2560            })
2561            .returning(move |_, data| {
2562                let bob_mgr_clone = bob_mgr_clone.clone();
2563                Box::pin(async move {
2564                    bob_mgr_clone
2565                        .dispatch_message(
2566                            alice_pseudonym,
2567                            ApplicationDataIn {
2568                                data: data.data,
2569                                packet_info: Default::default(),
2570                            },
2571                        )
2572                        ?;
2573                    Ok(())
2574                })
2575            });
2576
2577        const NEXT_BALANCER_TARGET: u64 = 50;
2578
2579        // Alice sends also the KeepAlive messages reporting the updated balancer target
2580        let bob_mgr_clone = bob_mgr.clone();
2581        alice_transport
2582            .expect_send_message()
2583            .times(5..)
2584            //.in_sequence(&mut sequence)
2585            .withf(move |peer, data| {
2586                start_msg_match(data, |msg| matches!(msg, StartProtocol::KeepAlive(ka) if ka.flags.contains(KeepAliveFlag::BalancerTarget) && ka.additional_data == NEXT_BALANCER_TARGET))
2587                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
2588            })
2589            .returning(move |_, data| {
2590                let bob_mgr_clone = bob_mgr_clone.clone();
2591                Box::pin(async move {
2592                    bob_mgr_clone
2593                        .dispatch_message(
2594                            alice_pseudonym,
2595                            ApplicationDataIn {
2596                                data: data.data,
2597                                packet_info: Default::default(),
2598                            },
2599                        )
2600                        ?;
2601                    Ok(())
2602                })
2603            });
2604
2605        // Bob sends at least 1 Keep Alive back reporting its SURB estimate
2606        let alice_mgr_clone = alice_mgr.clone();
2607        bob_transport
2608            .expect_send_message()
2609            .times(1..)
2610            //.in_sequence(&mut open_sequence)
2611            .withf(move |peer, data| {
2612                start_msg_match(data, |msg| matches!(msg, StartProtocol::KeepAlive(ka) if ka.flags.contains(KeepAliveFlag::BalancerState) && ka.additional_data > 0))
2613                    && matches!(peer, DestinationRouting::Return(SurbMatcher::Pseudonym(p)) if p == &alice_pseudonym)
2614            })
2615            .returning(move |_, data| {
2616                let alice_mgr_clone = alice_mgr_clone.clone();
2617                Box::pin(async move {
2618                    alice_mgr_clone
2619                        .dispatch_message(
2620                            alice_pseudonym,
2621                            ApplicationDataIn {
2622                                data: data.data,
2623                                packet_info: Default::default(),
2624                            },
2625                        )
2626                        ?;
2627                    Ok(())
2628                })
2629            });
2630
2631        // Alice sends the terminating segment to close the Session
2632        let bob_mgr_clone = bob_mgr.clone();
2633        alice_transport
2634            .expect_send_message()
2635            .once()
2636            //.in_sequence(&mut sequence)
2637            .withf(move |peer, data| {
2638                hopr_protocol_session::types::SessionMessage::<{ ApplicationData::PAYLOAD_SIZE }>::try_from(
2639                    data.data.plain_text.as_ref(),
2640                )
2641                .ok()
2642                .and_then(|m| m.try_as_segment())
2643                .map(|s| s.is_terminating())
2644                .unwrap_or(false)
2645                    && matches!(peer, DestinationRouting::Forward { destination, .. } if destination.as_ref() == &bob_peer.into())
2646            })
2647            .returning(move |_, data| {
2648                let bob_mgr_clone = bob_mgr_clone.clone();
2649                Box::pin(async move {
2650                    bob_mgr_clone
2651                        .dispatch_message(
2652                            alice_pseudonym,
2653                            ApplicationDataIn {
2654                                data: data.data,
2655                                packet_info: Default::default(),
2656                            },
2657                        )
2658                        ?;
2659                    Ok(())
2660                })
2661            });
2662
2663        let mut ahs = Vec::new();
2664
2665        // Start Alice
2666        let (new_session_tx_alice, _) = futures::channel::mpsc::channel(1024);
2667        let (alice_sender, alice_handle) = mock_packet_planning(alice_transport);
2668        ahs.extend(alice_mgr.start(alice_sender.clone(), new_session_tx_alice)?);
2669        assert!(alice_mgr.is_started());
2670
2671        // Start Bob
2672        let (new_session_tx_bob, new_session_rx_bob) = futures::channel::mpsc::channel(1024);
2673        let (bob_sender, bob_handle) = mock_packet_planning(bob_transport);
2674        ahs.extend(bob_mgr.start(bob_sender.clone(), new_session_tx_bob)?);
2675        assert!(bob_mgr.is_started());
2676
2677        let target = SealedHost::Plain("127.0.0.1:80".parse()?);
2678
2679        let balancer_cfg = SurbBalancerConfig {
2680            target_surb_buffer_size: INITIAL_BALANCER_TARGET,
2681            max_surbs_per_sec: 100,
2682            ..Default::default()
2683        };
2684
2685        pin_mut!(new_session_rx_bob);
2686        let (alice_session, bob_session) = timeout(
2687            Duration::from_secs(2),
2688            futures::future::join(
2689                alice_mgr.new_session(
2690                    bob_peer,
2691                    SessionTarget::TcpStream(target.clone()),
2692                    SessionClientConfig {
2693                        pseudonym: alice_pseudonym.into(),
2694                        capabilities: Capability::Segmentation.into(),
2695                        surb_management: Some(balancer_cfg),
2696                        ..Default::default()
2697                    },
2698                ),
2699                new_session_rx_bob.next(),
2700            ),
2701        )
2702        .await?;
2703
2704        let mut alice_session = alice_session?;
2705        let bob_session = bob_session.ok_or(anyhow!("bob must get an incoming session"))?;
2706
2707        assert!(matches!(bob_session.target, SessionTarget::TcpStream(host) if host == target));
2708
2709        assert_eq!(
2710            Some(balancer_cfg),
2711            alice_mgr.get_surb_balancer_config(alice_session.id())?
2712        );
2713
2714        let remote_cfg = bob_mgr
2715            .get_surb_balancer_config(bob_session.session.id())?
2716            .ok_or(anyhow!("no remote config at bob"))?;
2717        assert_eq!(remote_cfg.target_surb_buffer_size, balancer_cfg.target_surb_buffer_size);
2718        assert_eq!(
2719            remote_cfg.max_surbs_per_sec,
2720            remote_cfg.target_surb_buffer_size
2721                / bob_cfg
2722                    .minimum_surb_buffer_duration
2723                    .max(MIN_SURB_BUFFER_DURATION)
2724                    .as_secs()
2725        );
2726
2727        // Let the Surb balancer send enough KeepAlive messages
2728        tokio::time::sleep(Duration::from_millis(1500)).await;
2729
2730        let new_balancer_cfg = SurbBalancerConfig {
2731            target_surb_buffer_size: NEXT_BALANCER_TARGET,
2732            max_surbs_per_sec: 100,
2733            ..Default::default()
2734        };
2735
2736        // Update to a higher target
2737        alice_mgr.update_surb_balancer_config(alice_session.id(), new_balancer_cfg)?;
2738
2739        // Let the Surb balancer send enough KeepAlive messages
2740        tokio::time::sleep(Duration::from_millis(1500)).await;
2741
2742        // Bob should know about the updated target
2743        let remote_cfg = bob_mgr
2744            .get_surb_balancer_config(bob_session.session.id())?
2745            .ok_or(anyhow!("no remote config at bob"))?;
2746        assert_eq!(
2747            remote_cfg.target_surb_buffer_size,
2748            new_balancer_cfg.target_surb_buffer_size
2749        );
2750        assert_eq!(
2751            remote_cfg.max_surbs_per_sec,
2752            new_balancer_cfg.target_surb_buffer_size / bob_cfg.minimum_surb_buffer_duration.as_secs()
2753        );
2754
2755        let (alice_surb_sent, alice_surb_used) = alice_mgr.get_surb_level_estimates(alice_session.id())?;
2756        let (bob_surb_recv, bob_surb_used) = bob_mgr.get_surb_level_estimates(bob_session.session.id())?;
2757
2758        alice_session.close().await?;
2759
2760        assert!(alice_surb_sent > 0, "alice must've sent surbs");
2761        assert!(bob_surb_recv > 0, "bob must've received surbs");
2762        assert!(
2763            bob_surb_recv <= alice_surb_sent,
2764            "bob cannot receive more surbs than alice sent"
2765        );
2766
2767        assert!(alice_surb_used > 0, "alice must see bob used surbs");
2768        assert!(bob_surb_used > 0, "bob must've used surbs");
2769        assert!(
2770            alice_surb_used <= bob_surb_used,
2771            "alice cannot see bob used more surbs than bob actually used"
2772        );
2773
2774        tokio::time::sleep(Duration::from_millis(300)).await;
2775        assert!(matches!(
2776            alice_mgr.ping_session(alice_session.id()).await,
2777            Err(TransportSessionError::Manager(SessionManagerError::NonExistingSession))
2778        ));
2779
2780        futures::stream::iter(ahs)
2781            .for_each(|ah| async move { ah.abort() })
2782            .await;
2783
2784        // Cleanup: close senders and await handles
2785        alice_sender.close_channel();
2786        bob_sender.close_channel();
2787        let _ = alice_handle.await;
2788        let _ = bob_handle.await;
2789
2790        Ok(())
2791    }
2792
2793    #[test_log::test(tokio::test)]
2794    async fn session_manager_should_supersede_stale_session_on_reinitiation_with_same_pseudonym() -> anyhow::Result<()>
2795    {
2796        use hopr_utils::network_types::prelude::SealedHost;
2797
2798        let bob_mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
2799            SessionManager::new(Default::default());
2800
2801        // Start the manager (required for handling incoming sessions)
2802        let mut transport = MockMsgSender::new();
2803        transport
2804            .expect_send_message()
2805            .times(2)
2806            .returning(|_, _| futures::future::ok(()).boxed());
2807
2808        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2809        // Spawn a task to receive new session notifications
2810        let _notifications = tokio::spawn(async move {
2811            pin_mut!(new_session_rx);
2812            while let Some(_session) = new_session_rx.next().await {
2813                // Just drain the channel
2814            }
2815        });
2816        let (sender, _handle) = mock_packet_planning(transport);
2817        bob_mgr.start(sender.clone(), new_session_tx)?;
2818        assert!(bob_mgr.is_started());
2819
2820        let pseudonym = HoprPseudonym::random();
2821
2822        // First session initiation - should succeed
2823        let result = bob_mgr
2824            .handle_incoming_session_initiation(
2825                pseudonym,
2826                StartInitiation {
2827                    challenge: MIN_CHALLENGE,
2828                    target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
2829                    capabilities: ByteCapabilities(Capabilities::empty()),
2830                    additional_data: 0,
2831                },
2832            )
2833            .await;
2834
2835        assert!(result.is_ok(), "first session initiation should succeed");
2836
2837        // Verify one session exists
2838        let active = bob_mgr.active_sessions();
2839        assert_eq!(active.len(), 1, "should have exactly one active session");
2840
2841        // Second session initiation with the same pseudonym: the stale session is
2842        // closed and the new initiation takes the slot over (a re-initiation means
2843        // the initiator has lost or abandoned its side of the old session).
2844        let result = bob_mgr
2845            .handle_incoming_session_initiation(
2846                pseudonym,
2847                StartInitiation {
2848                    challenge: MIN_CHALLENGE + 1,
2849                    target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
2850                    capabilities: ByteCapabilities(Capabilities::empty()),
2851                    additional_data: 0,
2852                },
2853            )
2854            .await;
2855
2856        assert!(result.is_ok(), "re-initiation should supersede the stale session");
2857
2858        // The stale session must have been replaced, not duplicated
2859        let active = bob_mgr.active_sessions();
2860        assert_eq!(active.len(), 1, "should still have exactly one active session");
2861
2862        // Cleanup: close sender and await handle
2863        sender.close_channel();
2864        let _ = _handle.await;
2865
2866        Ok(())
2867    }
2868
2869    #[test_log::test(tokio::test)]
2870    async fn session_manager_should_return_error_when_pinging_non_existent_session() -> anyhow::Result<()> {
2871        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
2872            SessionManager::new(Default::default());
2873
2874        let transport = MockMsgSender::new();
2875        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2876        let _notifications = tokio::spawn(async move {
2877            pin_mut!(new_session_rx);
2878            while let Some(_session) = new_session_rx.next().await {}
2879        });
2880        let (sender, _handle) = mock_packet_planning(transport);
2881        mgr.start(sender.clone(), new_session_tx)?;
2882        assert!(mgr.is_started());
2883
2884        let fake_session_id = HoprPseudonym::random();
2885        let result = mgr.ping_session(&fake_session_id).await;
2886
2887        assert!(result.is_err());
2888        assert!(matches!(
2889            result.unwrap_err(),
2890            TransportSessionError::Manager(SessionManagerError::NonExistingSession)
2891        ));
2892
2893        // Cleanup: close sender and await handle
2894        sender.close_channel();
2895        let _ = _handle.await;
2896
2897        Ok(())
2898    }
2899
2900    #[test_log::test(tokio::test)]
2901    async fn session_manager_should_return_false_when_closing_non_existent_session() -> anyhow::Result<()> {
2902        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
2903            SessionManager::new(Default::default());
2904
2905        let transport = MockMsgSender::new();
2906        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2907        let _notifications = tokio::spawn(async move {
2908            pin_mut!(new_session_rx);
2909            while let Some(_session) = new_session_rx.next().await {}
2910        });
2911        let (sender, _handle) = mock_packet_planning(transport);
2912        mgr.start(sender.clone(), new_session_tx)?;
2913        assert!(mgr.is_started());
2914
2915        let fake_session_id = HoprPseudonym::random();
2916        let result = mgr.close_session(&fake_session_id);
2917
2918        assert!(!result, "closing non-existent session should return false");
2919
2920        Ok(())
2921    }
2922
2923    #[test_log::test(tokio::test)]
2924    async fn session_manager_should_return_error_when_updating_surb_config_for_non_existent_session()
2925    -> anyhow::Result<()> {
2926        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
2927            SessionManager::new(Default::default());
2928
2929        let transport = MockMsgSender::new();
2930        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2931        let _notifications = tokio::spawn(async move {
2932            pin_mut!(new_session_rx);
2933            while let Some(_session) = new_session_rx.next().await {}
2934        });
2935        let (sender, _handle) = mock_packet_planning(transport);
2936        mgr.start(sender.clone(), new_session_tx)?;
2937        assert!(mgr.is_started());
2938
2939        let fake_session_id = HoprPseudonym::random();
2940        let result = mgr.update_surb_balancer_config(&fake_session_id, SurbBalancerConfig::default());
2941
2942        assert!(result.is_err());
2943
2944        // Cleanup: close sender and await handle
2945        sender.close_channel();
2946        let _ = _handle.await;
2947
2948        Ok(())
2949    }
2950
2951    #[test_log::test(tokio::test)]
2952    async fn session_manager_should_return_error_when_getting_surb_config_for_non_existent_session()
2953    -> anyhow::Result<()> {
2954        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
2955            SessionManager::new(Default::default());
2956
2957        let transport = MockMsgSender::new();
2958        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2959        let _notifications = tokio::spawn(async move {
2960            pin_mut!(new_session_rx);
2961            while let Some(_session) = new_session_rx.next().await {}
2962        });
2963        let (sender, _handle) = mock_packet_planning(transport);
2964        mgr.start(sender.clone(), new_session_tx)?;
2965        assert!(mgr.is_started());
2966
2967        let fake_session_id = HoprPseudonym::random();
2968        let result = mgr.get_surb_balancer_config(&fake_session_id);
2969
2970        assert!(result.is_err());
2971        assert!(matches!(
2972            result.unwrap_err(),
2973            TransportSessionError::Manager(SessionManagerError::NonExistingSession)
2974        ));
2975
2976        // Cleanup: close sender and await handle
2977        sender.close_channel();
2978        let _ = _handle.await;
2979
2980        Ok(())
2981    }
2982
2983    #[test_log::test(tokio::test)]
2984    async fn session_manager_should_return_error_when_getting_surb_estimates_for_non_existent_session()
2985    -> anyhow::Result<()> {
2986        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
2987            SessionManager::new(Default::default());
2988
2989        let transport = MockMsgSender::new();
2990        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
2991        let _notifications = tokio::spawn(async move {
2992            pin_mut!(new_session_rx);
2993            while let Some(_session) = new_session_rx.next().await {}
2994        });
2995        let (sender, _handle) = mock_packet_planning(transport);
2996        mgr.start(sender.clone(), new_session_tx)?;
2997        assert!(mgr.is_started());
2998
2999        let fake_session_id = HoprPseudonym::random();
3000        let result = mgr.get_surb_level_estimates(&fake_session_id);
3001
3002        assert!(result.is_err());
3003        assert!(matches!(
3004            result.unwrap_err(),
3005            TransportSessionError::Manager(SessionManagerError::NonExistingSession)
3006        ));
3007
3008        // Cleanup: close sender and await handle
3009        sender.close_channel();
3010        let _ = _handle.await;
3011
3012        Ok(())
3013    }
3014
3015    /// Verifies the `HoprStartProtocol::SessionError` match arm (line 689) in the
3016    /// `session_start_protocol_processor` task by calling `handle_session_error` directly.
3017    ///
3018    /// When a `SessionError` message is delivered while a `new_session` call is awaiting,
3019    /// `handle_session_error` retrieves the pending challenge from `session_initiations`,
3020    /// sends the error down the channel, and `new_session` propagates it as `Rejected`.
3021    #[test_log::test(tokio::test)]
3022    async fn handle_session_error_propagates_peer_rejection_to_pending_new_session() -> anyhow::Result<()> {
3023        let mgr: SessionManager<UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
3024            SessionManager::new(Default::default());
3025
3026        let mut transport = MockMsgSender::new();
3027        // new_session sends StartSession (succeeds), then waits for SessionEstablished.
3028        // We inject the error before it arrives.
3029        transport
3030            .expect_send_message()
3031            .returning(|_, _| futures::future::ok(()).boxed());
3032
3033        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3034        let _notifications = tokio::spawn(async move {
3035            pin_mut!(new_session_rx);
3036            while let Some(_session) = new_session_rx.next().await {}
3037        });
3038        let (sender, _handle) = mock_packet_planning(transport);
3039        mgr.start(sender.clone(), new_session_tx)?;
3040        assert!(mgr.is_started());
3041
3042        // Spawn new_session so it is blocked waiting for the session establishment response.
3043        let mgr_clone = mgr.clone();
3044        let peer_address: Address = (&ChainKeypair::random()).into();
3045        let handle = tokio::spawn(async move {
3046            mgr_clone
3047                .new_session(
3048                    peer_address,
3049                    SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3050                    SessionClientConfig {
3051                        surb_management: None,
3052                        ..Default::default()
3053                    },
3054                )
3055                .await
3056        });
3057
3058        // Give new_session time to insert the challenge into session_initiations.
3059        let challenge = tokio::time::timeout(Duration::from_secs(1), async {
3060            loop {
3061                if let Some((ch, _)) = mgr.session_initiations.iter().next() {
3062                    break *ch;
3063                }
3064                tokio::time::sleep(Duration::from_millis(10)).await;
3065            }
3066        })
3067        .await
3068        .context("new_session did not insert a challenge into session_initiations")?;
3069
3070        // Inject the SessionError with the matching challenge before SessionEstablished arrives.
3071        let error_type = StartErrorType {
3072            challenge,
3073            reason: StartErrorReason::NoSlotsAvailable,
3074        };
3075        mgr.handle_session_error(error_type).await?;
3076
3077        // new_session must propagate the error as Rejected.
3078        let result = handle.await?;
3079        match result {
3080            Ok(_session) => panic!("expected rejection error, got session"),
3081            Err(e) => {
3082                assert!(matches!(
3083                    e,
3084                    TransportSessionError::Rejected(StartErrorReason::NoSlotsAvailable)
3085                ));
3086            }
3087        }
3088
3089        sender.close_channel();
3090        let _ = _handle.await;
3091        Ok(())
3092    }
3093
3094    #[test_log::test(tokio::test)]
3095    async fn session_manager_should_reject_new_session_when_max_sessions_reached() -> anyhow::Result<()> {
3096        use hopr_utils::network_types::prelude::SealedHost;
3097
3098        // Create manager with max 1 session
3099        let cfg = SessionManagerConfig {
3100            maximum_sessions: 1,
3101            ..Default::default()
3102        };
3103        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
3104            SessionManager::new(cfg);
3105
3106        let mut transport = MockMsgSender::new();
3107        transport
3108            .expect_send_message()
3109            .times(2)
3110            .returning(|_, _| futures::future::ok(()).boxed());
3111
3112        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3113        let _notifications = tokio::spawn(async move {
3114            pin_mut!(new_session_rx);
3115            while let Some(_session) = new_session_rx.next().await {}
3116        });
3117        let (sender, _handle) = mock_packet_planning(transport);
3118        mgr.start(sender.clone(), new_session_tx)?;
3119        assert!(mgr.is_started());
3120
3121        // First session - should succeed
3122        let pseudonym1 = HoprPseudonym::random();
3123        mgr.handle_incoming_session_initiation(
3124            pseudonym1,
3125            StartInitiation {
3126                challenge: MIN_CHALLENGE,
3127                target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3128                capabilities: ByteCapabilities(Capabilities::empty()),
3129                additional_data: 0,
3130            },
3131        )
3132        .await?;
3133
3134        // Verify one session exists
3135        assert_eq!(mgr.active_sessions().len(), 1);
3136
3137        // Second session - should fail with TooManySessions
3138        let pseudonym2 = HoprPseudonym::random();
3139        let _result = mgr
3140            .handle_incoming_session_initiation(
3141                pseudonym2,
3142                StartInitiation {
3143                    challenge: MIN_CHALLENGE,
3144                    target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3145                    capabilities: ByteCapabilities(Capabilities::empty()),
3146                    additional_data: 0,
3147                },
3148            )
3149            .await;
3150
3151        // The error is handled internally (sends SessionError), so result is Ok
3152        // But we can verify no new session was added
3153        assert_eq!(mgr.active_sessions().len(), 1);
3154
3155        // Cleanup: close sender and await handle
3156        sender.close_channel();
3157        let _ = _handle.await;
3158
3159        Ok(())
3160    }
3161
3162    /// Verifies the early `TooManySessions` return at the top of `new_session` (line 767).
3163    /// Unlike `session_manager_should_reject_new_session_when_max_sessions_reached`, which fills
3164    /// incoming slots and hits the slot-guard path at line 957, this test fills all `maximum_sessions`
3165    /// slots so that the `if self.cfg.maximum_sessions <= self.sessions.entry_count()` check fires
3166    /// before any message is sent.
3167    #[test_log::test(tokio::test)]
3168    async fn new_session_returns_too_many_sessions_when_cache_is_full() -> anyhow::Result<()> {
3169        use hopr_utils::network_types::prelude::SealedHost;
3170
3171        let cfg = SessionManagerConfig {
3172            maximum_sessions: 2,
3173            idle_timeout: Duration::from_secs(3600),
3174            ..Default::default()
3175        };
3176        let mgr: SessionManager<UnboundedSender<(DestinationRouting, ApplicationDataOut)>> = SessionManager::new(cfg);
3177
3178        let mut transport = MockMsgSender::new();
3179        // Two incoming sessions: first sends SessionEstablished, second sends SessionError (no slots).
3180        transport
3181            .expect_send_message()
3182            .times(2)
3183            .returning(|_, _| futures::future::ok(()).boxed());
3184
3185        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3186        let _notifications = tokio::spawn(async move {
3187            pin_mut!(new_session_rx);
3188            while let Some(_session) = new_session_rx.next().await {}
3189        });
3190        let (sender, _handle) = mock_packet_planning(transport);
3191        mgr.start(sender.clone(), new_session_tx)?;
3192        assert!(mgr.is_started());
3193
3194        // Fill the cache with two incoming sessions (Exits).
3195        for i in 0..2 {
3196            let pseudonym = HoprPseudonym::random();
3197            mgr.handle_incoming_session_initiation(
3198                pseudonym,
3199                StartInitiation {
3200                    challenge: MIN_CHALLENGE + i as u64,
3201                    target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3202                    capabilities: ByteCapabilities(Capabilities::empty()),
3203                    additional_data: 0,
3204                },
3205            )
3206            .await?;
3207        }
3208        assert_eq!(mgr.active_sessions().len(), 2);
3209
3210        // Third outgoing call hits the early return before sending anything.
3211        let result = mgr
3212            .new_session(
3213                Address::from(&ChainKeypair::random()),
3214                SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3215                SessionClientConfig {
3216                    surb_management: None,
3217                    ..Default::default()
3218                },
3219            )
3220            .await;
3221
3222        assert!(result.is_err());
3223        assert!(matches!(
3224            result.unwrap_err(),
3225            TransportSessionError::Manager(SessionManagerError::TooManySessions)
3226        ));
3227
3228        sender.close_channel();
3229        let _ = _handle.await;
3230        Ok(())
3231    }
3232
3233    /// Verifies that `session_initiations` is cleaned up when `new_session` fails to
3234    /// send the StartSession message (e.g. the underlying channel is closed).
3235    #[test_log::test(tokio::test)]
3236    async fn new_session_removes_challenge_on_send_failure() -> anyhow::Result<()> {
3237        let mgr: SessionManager<UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
3238            SessionManager::new(Default::default());
3239
3240        // Create a channel whose receiver is dropped immediately.  When the mock
3241        // transport tries to `send` over this channel the call will return an error,
3242        // which propagates up through `send_via_msg_sender` as
3243        // `TransportSessionError::packet_sending`.
3244        let (tx, rx) = futures::channel::mpsc::unbounded();
3245        drop(rx);
3246
3247        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3248        let _notifications = tokio::spawn(async move {
3249            pin_mut!(new_session_rx);
3250            while let Some(_session) = new_session_rx.next().await {}
3251        });
3252        mgr.start(tx, new_session_tx)?;
3253        assert!(mgr.is_started());
3254
3255        // Verify that sending fails because the receiver is gone.
3256        let result = mgr
3257            .new_session(
3258                Address::from(&ChainKeypair::random()),
3259                SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3260                SessionClientConfig {
3261                    surb_management: None,
3262                    ..Default::default()
3263                },
3264            )
3265            .await;
3266
3267        assert!(result.is_err());
3268        // The challenge must have been removed from `session_initiations` even
3269        // though the send failed.
3270        assert_eq!(
3271            mgr.session_initiations.entry_count(),
3272            0,
3273            "session_initiations was not cleaned up after send failure"
3274        );
3275
3276        Ok(())
3277    }
3278
3279    /// Verifies that `session_initiations` is cleaned up when the session initiation
3280    /// times out waiting for a response (neither `SessionEstablished` nor
3281    /// `SessionError` arrives).
3282    #[test_log::test(tokio::test)]
3283    async fn new_session_removes_challenge_on_timeout() -> anyhow::Result<()> {
3284        let cfg = SessionManagerConfig {
3285            initiation_timeout_base: Duration::from_millis(100),
3286            ..Default::default()
3287        };
3288
3289        let alice_mgr = SessionManager::new(cfg);
3290        let bob_mgr = SessionManager::new(Default::default());
3291
3292        let bob_peer: Address = (&ChainKeypair::random()).into();
3293
3294        let mut alice_transport = MockMsgSender::new();
3295        let bob_transport = MockMsgSender::new();
3296
3297        // Alice sends the StartSession message; Bob never responds.
3298        alice_transport
3299            .expect_send_message()
3300            .once()
3301            .returning(|_, _| futures::future::ok(()).boxed());
3302
3303        let (alice_sender, _alice_handle) = mock_packet_planning(alice_transport);
3304        let (new_session_tx_alice, _) = futures::channel::mpsc::channel(1024);
3305        alice_mgr.start(alice_sender.clone(), new_session_tx_alice)?;
3306        assert!(alice_mgr.is_started());
3307
3308        let (bob_sender, _bob_handle) = mock_packet_planning(bob_transport);
3309        let (new_session_tx_bob, _) = futures::channel::mpsc::channel(1024);
3310        bob_mgr.start(bob_sender.clone(), new_session_tx_bob)?;
3311        assert!(bob_mgr.is_started());
3312
3313        // Record how many entries are in `session_initiations` before the call.
3314        assert_eq!(alice_mgr.session_initiations.entry_count(), 0);
3315
3316        let result = alice_mgr
3317            .new_session(
3318                bob_peer,
3319                SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3320                SessionClientConfig {
3321                    capabilities: None.into(),
3322                    pseudonym: None,
3323                    surb_management: None,
3324                    ..Default::default()
3325                },
3326            )
3327            .await;
3328
3329        assert!(matches!(result, Err(TransportSessionError::Timeout)));
3330        // The pending challenge must have been removed from `session_initiations`
3331        // after the timeout error propagated.
3332        assert_eq!(
3333            alice_mgr.session_initiations.entry_count(),
3334            0,
3335            "session_initiations was not cleaned up after timeout"
3336        );
3337
3338        Ok(())
3339    }
3340
3341    #[test_log::test(tokio::test)]
3342    async fn session_manager_should_return_unknown_data_error_when_dispatching_to_unknown_session() -> anyhow::Result<()>
3343    {
3344        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
3345            SessionManager::new(Default::default());
3346
3347        let transport = MockMsgSender::new();
3348        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3349        let _notifications = tokio::spawn(async move {
3350            pin_mut!(new_session_rx);
3351            while let Some(_session) = new_session_rx.next().await {}
3352        });
3353        let (sender, _handle) = mock_packet_planning(transport);
3354        mgr.start(sender.clone(), new_session_tx)?;
3355        assert!(mgr.is_started());
3356
3357        // Send data with session application tag but no session exists
3358        let pseudonym = HoprPseudonym::random();
3359        let result = mgr.dispatch_message(
3360            pseudonym,
3361            ApplicationDataIn {
3362                data: ApplicationData::new(SESSION_APPLICATION_TAG, b"test data")?,
3363                packet_info: Default::default(),
3364            },
3365        );
3366
3367        assert!(result.is_err());
3368        assert!(matches!(result.unwrap_err(), TransportSessionError::UnknownData));
3369
3370        // Cleanup: close sender and await handle
3371        sender.close_channel();
3372        let _ = _handle.await;
3373
3374        Ok(())
3375    }
3376
3377    #[test_log::test(tokio::test)]
3378    async fn session_manager_should_return_true_when_closing_existing_session() -> anyhow::Result<()> {
3379        use hopr_utils::network_types::prelude::SealedHost;
3380
3381        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
3382            SessionManager::new(Default::default());
3383
3384        let mut transport = MockMsgSender::new();
3385        transport
3386            .expect_send_message()
3387            .once()
3388            .returning(|_, _| futures::future::ok(()).boxed());
3389
3390        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3391        let _notifications = tokio::spawn(async move {
3392            pin_mut!(new_session_rx);
3393            while let Some(_session) = new_session_rx.next().await {}
3394        });
3395        let (sender, _handle) = mock_packet_planning(transport);
3396        mgr.start(sender.clone(), new_session_tx)?;
3397        assert!(mgr.is_started());
3398
3399        // Create a session
3400        let pseudonym = HoprPseudonym::random();
3401        mgr.handle_incoming_session_initiation(
3402            pseudonym,
3403            StartInitiation {
3404                challenge: MIN_CHALLENGE,
3405                target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3406                capabilities: ByteCapabilities(Capabilities::empty()),
3407                additional_data: 0,
3408            },
3409        )
3410        .await?;
3411
3412        // Verify session exists
3413        assert_eq!(mgr.active_sessions().len(), 1);
3414
3415        // Close the session - should return true
3416        let result = mgr.close_session(&pseudonym);
3417        assert!(result, "closing existing session should return true");
3418
3419        // Verify session is closed
3420        assert_eq!(mgr.active_sessions().len(), 0);
3421
3422        // Cleanup: close sender and await handle
3423        sender.close_channel();
3424        let _ = _handle.await;
3425
3426        Ok(())
3427    }
3428
3429    #[test_log::test(tokio::test)]
3430    async fn session_manager_should_update_buffer_level_on_keep_alive_with_balancer_state_flag() -> anyhow::Result<()> {
3431        use std::sync::atomic::Ordering;
3432
3433        let alice_pseudonym = HoprPseudonym::random();
3434        let session_id = alice_pseudonym;
3435        let initial_buffer_level = 100u64;
3436        let new_buffer_level = 200u64;
3437
3438        let balancer_cfg = SurbBalancerConfig {
3439            target_surb_buffer_size: 1000,
3440            max_surbs_per_sec: 100,
3441            ..Default::default()
3442        };
3443
3444        let alice_mgr =
3445            SessionManager::<UnboundedSender<(DestinationRouting, ApplicationDataOut)>>::new(Default::default());
3446
3447        let (new_session_tx, _) = futures::channel::mpsc::channel(1024);
3448        let (mock_sender, _) = futures::channel::mpsc::unbounded();
3449        let _ahs = alice_mgr.start(mock_sender, new_session_tx)?;
3450        assert!(alice_mgr.is_started());
3451
3452        let (dummy_tx, _) = crossfire::mpsc::bounded_blocking_async::<ApplicationDataIn>(SESSION_FORWARD_CAPACITY);
3453        let peer_address: Address = (&ChainKeypair::random()).into();
3454        alice_mgr.sessions.insert(
3455            session_id,
3456            SessionSlot {
3457                session_tx: dummy_tx,
3458                routing_opts: DestinationRouting::Forward {
3459                    destination: Box::new(peer_address.into()),
3460                    pseudonym: Some(alice_pseudonym),
3461                    forward_options: RoutingOptions::Hops(hopr_api::types::primitive::bounded::BoundedSize::MIN),
3462                    return_options: RoutingOptions::Hops(hopr_api::types::primitive::bounded::BoundedSize::MIN).into(),
3463                },
3464                abort_handles: Default::default(),
3465                surb_mgmt: Arc::new(BalancerStateValues::from(balancer_cfg)),
3466                surb_estimator: Default::default(),
3467            },
3468        );
3469
3470        // Set initial buffer level
3471        let session_slot = alice_mgr.sessions.get(&session_id).unwrap();
3472        session_slot
3473            .surb_mgmt
3474            .buffer_level
3475            .store(initial_buffer_level, Ordering::Relaxed);
3476        drop(session_slot);
3477
3478        // Verify initial buffer level
3479        let session_slot = alice_mgr.sessions.get(&session_id).unwrap();
3480        assert_eq!(session_slot.surb_mgmt.buffer_level(), initial_buffer_level);
3481        drop(session_slot);
3482
3483        // Create keep-alive message with BalancerState flag
3484        let ka = KeepAliveMessage::<SessionId> {
3485            session_id,
3486            flags: KeepAliveFlag::BalancerState.into(),
3487            additional_data: new_buffer_level,
3488        };
3489        let app_data: ApplicationData = HoprStartProtocol::KeepAlive(ka).try_into()?;
3490        let app_data_in = ApplicationDataIn {
3491            data: app_data,
3492            packet_info: Default::default(),
3493        };
3494
3495        // Dispatch the keep-alive message
3496        alice_mgr.dispatch_message(alice_pseudonym, app_data_in)?;
3497
3498        // Poll until the background task has processed the keep-alive
3499        tokio::time::timeout(Duration::from_secs(1), async {
3500            loop {
3501                if let Some(slot) = alice_mgr.sessions.get(&session_id)
3502                    && slot.surb_mgmt.buffer_level() == new_buffer_level
3503                {
3504                    break;
3505                }
3506                tokio::time::sleep(Duration::from_millis(10)).await;
3507            }
3508        })
3509        .await
3510        .context("keep-alive BalancerState update timed out")?;
3511
3512        // Verify buffer level was updated
3513        let session_slot = alice_mgr.sessions.get(&session_id).unwrap();
3514        assert_eq!(
3515            session_slot.surb_mgmt.buffer_level(),
3516            new_buffer_level,
3517            "buffer level should be updated via keep-alive with BalancerState flag"
3518        );
3519
3520        Ok(())
3521    }
3522
3523    #[test_log::test(tokio::test)]
3524    async fn session_manager_should_update_target_on_keep_alive_with_balancer_target_flag() -> anyhow::Result<()> {
3525        use std::sync::atomic::Ordering;
3526
3527        let alice_pseudonym = HoprPseudonym::random();
3528        let session_id = alice_pseudonym;
3529        let initial_target = 1000u64;
3530        let new_target = 2000u64;
3531
3532        let balancer_cfg = SurbBalancerConfig {
3533            target_surb_buffer_size: initial_target,
3534            max_surbs_per_sec: 100,
3535            ..Default::default()
3536        };
3537
3538        let alice_mgr =
3539            SessionManager::<UnboundedSender<(DestinationRouting, ApplicationDataOut)>>::new(Default::default());
3540
3541        let (new_session_tx, _) = futures::channel::mpsc::channel(1024);
3542        let (mock_sender, _) = futures::channel::mpsc::unbounded();
3543        let _ahs = alice_mgr.start(mock_sender, new_session_tx)?;
3544        assert!(alice_mgr.is_started());
3545
3546        let (dummy_tx, _) = crossfire::mpsc::bounded_blocking_async::<ApplicationDataIn>(SESSION_FORWARD_CAPACITY);
3547        alice_mgr.sessions.insert(
3548            session_id,
3549            SessionSlot {
3550                session_tx: dummy_tx,
3551                routing_opts: DestinationRouting::Return(SurbMatcher::Pseudonym(alice_pseudonym)),
3552                abort_handles: Default::default(),
3553                surb_mgmt: Arc::new(BalancerStateValues::from(balancer_cfg)),
3554                surb_estimator: Default::default(),
3555            },
3556        );
3557
3558        // Verify initial target
3559        let session_slot = alice_mgr.sessions.get(&session_id).unwrap();
3560        assert_eq!(
3561            session_slot.surb_mgmt.controller_bounds().target(),
3562            initial_target,
3563            "initial target should be set"
3564        );
3565        drop(session_slot);
3566
3567        // Create keep-alive message with BalancerTarget flag
3568        let ka = KeepAliveMessage::<SessionId> {
3569            session_id,
3570            flags: KeepAliveFlag::BalancerTarget.into(),
3571            additional_data: new_target,
3572        };
3573        let app_data: ApplicationData = HoprStartProtocol::KeepAlive(ka).try_into()?;
3574        let app_data_in = ApplicationDataIn {
3575            data: app_data,
3576            packet_info: Default::default(),
3577        };
3578
3579        // Dispatch the keep-alive message
3580        alice_mgr.dispatch_message(alice_pseudonym, app_data_in)?;
3581
3582        // Poll until the background task has processed the keep-alive
3583        tokio::time::timeout(Duration::from_secs(1), async {
3584            loop {
3585                if let Some(slot) = alice_mgr.sessions.get(&session_id)
3586                    && slot.surb_mgmt.target_surb_buffer_size.load(Ordering::Relaxed) == new_target
3587                {
3588                    break;
3589                }
3590                tokio::time::sleep(Duration::from_millis(10)).await;
3591            }
3592        })
3593        .await
3594        .context("keep-alive BalancerTarget update timed out")?;
3595
3596        // Verify target was updated
3597        let session_slot = alice_mgr.sessions.get(&session_id).unwrap();
3598        assert_eq!(
3599            session_slot.surb_mgmt.target_surb_buffer_size.load(Ordering::Relaxed),
3600            new_target,
3601            "target buffer size should be updated via keep-alive with BalancerTarget flag"
3602        );
3603
3604        Ok(())
3605    }
3606
3607    #[test_log::test(tokio::test)]
3608    async fn session_manager_should_evict_idle_session_and_call_close_callback() -> anyhow::Result<()> {
3609        use hopr_utils::network_types::prelude::SealedHost;
3610
3611        let cfg = SessionManagerConfig {
3612            maximum_sessions: 1,
3613            idle_timeout: Duration::from_millis(100),
3614            ..Default::default()
3615        };
3616        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
3617            SessionManager::new(cfg);
3618
3619        let mut transport = MockMsgSender::new();
3620        transport
3621            .expect_send_message()
3622            .times(1)
3623            .returning(|_, _| futures::future::ok(()).boxed());
3624
3625        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3626        let _notifications = tokio::spawn(async move {
3627            pin_mut!(new_session_rx);
3628            while let Some(_session) = new_session_rx.next().await {}
3629        });
3630        let (sender, _handle) = mock_packet_planning(transport);
3631        mgr.start(sender.clone(), new_session_tx)?;
3632        assert!(mgr.is_started());
3633
3634        // Create first session
3635        let pseudonym1 = HoprPseudonym::random();
3636        mgr.handle_incoming_session_initiation(
3637            pseudonym1,
3638            StartInitiation {
3639                challenge: MIN_CHALLENGE,
3640                target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3641                capabilities: ByteCapabilities(Capabilities::empty()),
3642                additional_data: 0,
3643            },
3644        )
3645        .await?;
3646
3647        // Verify first session exists
3648        assert_eq!(mgr.active_sessions().len(), 1);
3649
3650        // Wait for the session to expire (idle_timeout = 100ms)
3651        tokio::time::sleep(Duration::from_millis(200)).await;
3652        mgr.sessions.run_pending_tasks();
3653
3654        // Verify session was evicted (cache should be empty now)
3655        assert_eq!(
3656            mgr.active_sessions().len(),
3657            0,
3658            "idle session should be evicted after timeout"
3659        );
3660
3661        Ok(())
3662    }
3663
3664    #[test_log::test(tokio::test)]
3665    async fn session_manager_should_reject_new_session_when_max_sessions_reached_no_eviction() -> anyhow::Result<()> {
3666        use hopr_utils::network_types::prelude::SealedHost;
3667
3668        // Create manager with max 1 session
3669        let cfg = SessionManagerConfig {
3670            maximum_sessions: 1,
3671            idle_timeout: Duration::from_secs(3600), // Long timeout so eviction doesn't happen
3672            ..Default::default()
3673        };
3674        let mgr: SessionManager<futures::channel::mpsc::UnboundedSender<(DestinationRouting, ApplicationDataOut)>> =
3675            SessionManager::new(cfg);
3676
3677        let mut transport = MockMsgSender::new();
3678        transport
3679            .expect_send_message()
3680            .times(2)
3681            .returning(|_, _| futures::future::ok(()).boxed());
3682
3683        let (new_session_tx, new_session_rx) = futures::channel::mpsc::channel(1);
3684        let _notifications = tokio::spawn(async move {
3685            pin_mut!(new_session_rx);
3686            while let Some(_session) = new_session_rx.next().await {}
3687        });
3688        let (sender, _handle) = mock_packet_planning(transport);
3689        mgr.start(sender.clone(), new_session_tx)?;
3690        assert!(mgr.is_started());
3691
3692        // Create first session
3693        let pseudonym1 = HoprPseudonym::random();
3694        mgr.handle_incoming_session_initiation(
3695            pseudonym1,
3696            StartInitiation {
3697                challenge: MIN_CHALLENGE,
3698                target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3699                capabilities: ByteCapabilities(Capabilities::empty()),
3700                additional_data: 0,
3701            },
3702        )
3703        .await?;
3704
3705        // Verify first session exists
3706        assert_eq!(mgr.active_sessions().len(), 1);
3707
3708        // Try to create second session - should be rejected (not evicted)
3709        let pseudonym2 = HoprPseudonym::random();
3710        let _result = mgr
3711            .handle_incoming_session_initiation(
3712                pseudonym2,
3713                StartInitiation {
3714                    challenge: MIN_CHALLENGE,
3715                    target: SessionTarget::TcpStream(SealedHost::Plain("127.0.0.1:80".parse()?)),
3716                    capabilities: ByteCapabilities(Capabilities::empty()),
3717                    additional_data: 0,
3718                },
3719            )
3720            .await;
3721
3722        // Should still have exactly 1 session (the first one)
3723        assert_eq!(
3724            mgr.active_sessions().len(),
3725            1,
3726            "should still have exactly one session - second session should be rejected"
3727        );
3728
3729        // The active session should be the first one (second was rejected)
3730        assert!(
3731            mgr.active_sessions().contains(&pseudonym1),
3732            "the first session should still be active"
3733        );
3734
3735        // Cleanup: close sender and await handle
3736        sender.close_channel();
3737        let _ = _handle.await;
3738
3739        Ok(())
3740    }
3741}