1use std::time::Duration;
2
3use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt};
4use hopr_api::types::internal::routing::DestinationRouting;
5use hopr_protocol_app::prelude::{ApplicationData, ApplicationDataOut};
6use hopr_protocol_start::{KeepAliveFlag, KeepAliveMessage};
7#[cfg(feature = "runtime-tokio")]
13pub use hopr_utils::network_types::utils::transfer_session;
14use hopr_utils::runtime::AbortHandle;
15use tracing::{Instrument, debug, error, instrument};
16
17use crate::{
18 AtomicSurbFlowEstimator, SessionId,
19 balancer::{BalancerStateValues, RateController, RateLimitStreamExt, SurbFlowEstimator},
20 errors::TransportSessionError,
21 types::HoprStartProtocol,
22};
23
24pub(crate) fn insert_into_next_slot<F, K, U, V>(
30 cache: &moka::sync::Cache<K, V>,
31 mut generator: F,
32 value_fn: U,
33 max_capacity: Option<u64>,
34) -> Option<(K, V)>
35where
36 F: FnMut(Option<K>) -> K,
37 K: Copy + std::hash::Hash + Eq + Send + Sync + 'static,
38 U: FnOnce(K) -> V,
39 V: Clone + Send + Sync + 'static,
40{
41 cache.run_pending_tasks();
42
43 if let Some(max) = max_capacity
46 && cache.entry_count() >= max
47 {
48 return None;
49 }
50
51 let value_fn = std::sync::Arc::new(parking_lot::Mutex::new(Some(value_fn)));
54
55 let initial = generator(None);
56 let mut next = initial;
57 loop {
58 let value_fn = value_fn.clone();
59 let insertion_result = cache.entry(next).and_compute_with(move |e| {
60 if e.is_none() {
61 let f = value_fn
62 .lock()
63 .take()
64 .expect("impossible: value_fn was already consumed");
65
66 moka::ops::compute::Op::Put(f(next))
67 } else {
68 moka::ops::compute::Op::Nop
69 }
70 });
71
72 if let moka::ops::compute::CompResult::Inserted(val) = insertion_result {
74 return Some((next, val.into_value()));
75 }
76
77 next = generator(Some(next));
79
80 if next == initial {
82 return None;
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
90pub(crate) enum SurbNotificationMode {
91 DoNotNotify,
93 Target,
95 Level(AtomicSurbFlowEstimator),
97}
98
99#[instrument(level = "debug", skip(sender, routing, notification_mode, cfg))]
101pub(crate) fn spawn_keep_alive_stream<S>(
102 session_id: SessionId,
103 sender: S,
104 routing: DestinationRouting,
105 notification_mode: SurbNotificationMode,
106 cfg: std::sync::Arc<BalancerStateValues>,
107) -> (RateController, AbortHandle)
108where
109 S: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Clone + Send + Sync + Unpin + 'static,
110 S::Error: std::error::Error + Send + Sync + 'static,
111{
112 let controller = RateController::new(0, Duration::from_secs(1));
114
115 let (ka_stream, abort_handle) = futures::stream::abortable(
117 futures::stream::repeat_with(move || match ¬ification_mode {
118 SurbNotificationMode::Target => HoprStartProtocol::KeepAlive(KeepAliveMessage {
119 session_id,
120 flags: KeepAliveFlag::BalancerTarget.into(),
121 additional_data: cfg.target_surb_buffer_size.load(std::sync::atomic::Ordering::Relaxed),
122 }),
123 SurbNotificationMode::Level(estimator) => HoprStartProtocol::KeepAlive(KeepAliveMessage {
124 session_id,
125 flags: KeepAliveFlag::BalancerState.into(),
126 additional_data: estimator.saturating_diff(),
127 }),
128 SurbNotificationMode::DoNotNotify => HoprStartProtocol::KeepAlive(KeepAliveMessage {
129 session_id,
130 flags: None.into(),
131 additional_data: 0,
132 }),
133 })
134 .rate_limit_with_controller(&controller),
135 );
136
137 let sender_clone = sender.clone();
138 let fwd_routing_clone = routing.clone();
139
140 debug!(%session_id, "spawning keep-alive stream");
142 let keep_alive_diag = hopr_utils::runtime::diagnostics::ConcurrentDiagnostics::new(
143 "session_keep_alive_try_for_each_concurrent",
144 module_path!(),
145 file!(),
146 line!(),
147 );
148 hopr_utils::runtime::prelude::spawn(hopr_utils::runtime::diagnostics::instrument(
149 ka_stream
150 .map(move |msg| {
151 ApplicationData::try_from(msg)
152 .map(|data| (fwd_routing_clone.clone(), ApplicationDataOut::with_no_packet_info(data)))
153 })
154 .map_err(TransportSessionError::from)
155 .try_for_each_concurrent(None, move |msg| {
156 let mut sender_clone = sender_clone.clone();
157 let keep_alive_diag = keep_alive_diag.clone();
158 keep_alive_diag.wrap(|| async move {
159 sender_clone
160 .send(msg)
161 .await
162 .map_err(TransportSessionError::packet_sending)
163 })
164 })
165 .then(move |res| {
166 match res {
167 Ok(_) => tracing::debug!(
168 component = "session",
169 %session_id,
170 task = "session keepalive",
171 "background task finished"
172 ),
173 Err(error) => error!(%session_id, %error, "keep-alive stream failed"),
174 }
175 futures::future::ready(())
176 })
177 .in_current_span(),
178 "session_keep_alive",
179 module_path!(),
180 file!(),
181 line!(),
182 ));
183
184 (controller, abort_handle)
185}
186
187#[cfg(test)]
188mod tests {
189 use anyhow::anyhow;
190
191 use super::*;
192
193 fn cycling_generator(prev: Option<u8>) -> u8 {
195 prev.map(|v| (v + 1) % 5).unwrap_or(0)
196 }
197
198 #[tokio::test]
200 async fn test_insert_into_next_slot_sequential() -> anyhow::Result<()> {
201 let cache = moka::sync::Cache::new(10);
202
203 for i in 0..5 {
204 let (k, v) = insert_into_next_slot(&cache, cycling_generator, |k| format!("foo_{k}"), Some(10u64))
205 .ok_or(anyhow!("should insert into slot {i}"))?;
206 assert_eq!(k, i);
207 assert_eq!(format!("foo_{i}"), v);
208 assert_eq!(Some(v), cache.get(&i));
209 }
210
211 Ok(())
212 }
213
214 #[tokio::test]
216 async fn test_insert_into_next_slot_returns_none_when_full() -> anyhow::Result<()> {
217 let cache = moka::sync::Cache::new(10);
218
219 for _ in 0..5 {
220 insert_into_next_slot(&cache, cycling_generator, |k| format!("foo_{k}"), Some(10u64))
221 .ok_or(anyhow!("precondition: should insert"))?;
222 }
223
224 assert!(
225 insert_into_next_slot(&cache, cycling_generator, |_| "foo".to_string(), Some(10u64)).is_none(),
226 "must not find slot when full"
227 );
228
229 Ok(())
230 }
231
232 #[tokio::test]
234 async fn test_insert_into_next_slot_capacity_one_rejects_second_key() -> anyhow::Result<()> {
235 let unit_cache = moka::sync::Cache::new(1);
236
237 let (k0, _v0) = insert_into_next_slot(&unit_cache, |prev| prev.map(|v| v + 1).unwrap_or(0), |k| k, Some(1u64))
238 .ok_or(anyhow!("first insertion must succeed"))?;
239 assert_eq!(k0, 0);
240
241 assert!(
242 insert_into_next_slot(&unit_cache, |prev| prev.map(|v| v + 1).unwrap_or(0), |k| k, Some(1u64)).is_none(),
243 "second distinct key must be rejected when cache capacity is 1"
244 );
245
246 Ok(())
247 }
248
249 #[tokio::test]
251 async fn test_insert_into_next_slot_rejected_insertion_does_not_evict() -> anyhow::Result<()> {
252 let unit_cache = moka::sync::Cache::new(1);
253
254 let (k0, v0) = insert_into_next_slot(&unit_cache, |prev| prev.map(|v| v + 1).unwrap_or(0), |k| k, Some(1u64))
255 .ok_or(anyhow!("first insertion must succeed"))?;
256
257 insert_into_next_slot(&unit_cache, |prev| prev.map(|v| v + 1).unwrap_or(0), |k| k, Some(1u64));
258
259 assert_eq!(
260 Some(v0),
261 unit_cache.get(&k0),
262 "first entry must still be present after rejection"
263 );
264
265 Ok(())
266 }
267}