1use parking_lot::RwLock;
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12use std::time::Duration;
13
14#[derive(Debug, Clone)]
26pub struct SessionConfig {
27 pub timeout: Duration,
29 pub max_bytes: u64,
31 pub max_concurrent_requests: usize,
33 pub request_timeout: Duration,
35 pub max_retries: usize,
37 pub backoff_base: Duration,
39}
40
41impl Default for SessionConfig {
42 fn default() -> Self {
43 Self {
44 timeout: Duration::from_secs(30),
45 max_bytes: 1024 * 1024 * 1024,
46 max_concurrent_requests: 16,
47 request_timeout: Duration::from_secs(10),
48 max_retries: 3,
49 backoff_base: Duration::from_millis(200),
50 }
51 }
52}
53
54impl SessionConfig {
55 pub fn high_throughput() -> Self {
57 Self {
58 timeout: Duration::from_secs(120),
59 max_bytes: 10 * 1024 * 1024 * 1024,
60 max_concurrent_requests: 64,
61 request_timeout: Duration::from_secs(30),
62 max_retries: 5,
63 backoff_base: Duration::from_millis(100),
64 }
65 }
66
67 pub fn low_latency() -> Self {
69 Self {
70 timeout: Duration::from_secs(5),
71 max_bytes: 100 * 1024 * 1024,
72 max_concurrent_requests: 8,
73 request_timeout: Duration::from_secs(2),
74 max_retries: 1,
75 backoff_base: Duration::from_millis(50),
76 }
77 }
78
79 pub fn backoff_for_retry(&self, retry: usize) -> Duration {
84 let exp = retry.min(6) as u32;
85 self.backoff_base.saturating_mul(2u32.pow(exp))
86 }
87}
88
89#[derive(Debug, Clone)]
93pub struct SessionMetricsSnapshot {
94 pub session_id: String,
96 pub bytes_transferred: u64,
98 pub blocks_requested: u64,
100 pub blocks_received: u64,
102 pub blocks_failed: u64,
104 pub total_retries: u64,
106 pub elapsed_ms: u64,
108 pub throughput_bps: f64,
110}
111
112pub struct SessionMetrics {
119 pub session_id: String,
121 bytes_transferred: AtomicU64,
122 blocks_requested: AtomicU64,
123 blocks_received: AtomicU64,
124 blocks_failed: AtomicU64,
125 total_retries: AtomicU64,
126 started_at_ms: u64,
127}
128
129impl SessionMetrics {
130 pub fn new(session_id: impl Into<String>, started_at_ms: u64) -> Arc<Self> {
134 Arc::new(Self {
135 session_id: session_id.into(),
136 bytes_transferred: AtomicU64::new(0),
137 blocks_requested: AtomicU64::new(0),
138 blocks_received: AtomicU64::new(0),
139 blocks_failed: AtomicU64::new(0),
140 total_retries: AtomicU64::new(0),
141 started_at_ms,
142 })
143 }
144
145 pub fn record_bytes(&self, bytes: u64) {
147 self.bytes_transferred.fetch_add(bytes, Ordering::Relaxed);
148 }
149
150 pub fn record_block_requested(&self) {
152 self.blocks_requested.fetch_add(1, Ordering::Relaxed);
153 }
154
155 pub fn record_block_received(&self) {
157 self.blocks_received.fetch_add(1, Ordering::Relaxed);
158 }
159
160 pub fn record_block_failed(&self) {
162 self.blocks_failed.fetch_add(1, Ordering::Relaxed);
163 }
164
165 pub fn record_retry(&self) {
167 self.total_retries.fetch_add(1, Ordering::Relaxed);
168 }
169
170 pub fn snapshot(&self, now_ms: u64) -> SessionMetricsSnapshot {
172 let bytes_transferred = self.bytes_transferred.load(Ordering::Acquire);
173 let elapsed_ms = now_ms.saturating_sub(self.started_at_ms);
174 let elapsed_secs = elapsed_ms as f64 / 1_000.0;
175 let throughput_bps = if elapsed_secs > 0.0 {
176 bytes_transferred as f64 / elapsed_secs
177 } else {
178 0.0
179 };
180
181 SessionMetricsSnapshot {
182 session_id: self.session_id.clone(),
183 bytes_transferred,
184 blocks_requested: self.blocks_requested.load(Ordering::Acquire),
185 blocks_received: self.blocks_received.load(Ordering::Acquire),
186 blocks_failed: self.blocks_failed.load(Ordering::Acquire),
187 total_retries: self.total_retries.load(Ordering::Acquire),
188 elapsed_ms,
189 throughput_bps,
190 }
191 }
192
193 pub fn bytes_transferred(&self) -> u64 {
195 self.bytes_transferred.load(Ordering::Relaxed)
196 }
197
198 pub fn blocks_received(&self) -> u64 {
200 self.blocks_received.load(Ordering::Relaxed)
201 }
202}
203
204impl std::fmt::Debug for SessionMetrics {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 f.debug_struct("SessionMetrics")
207 .field("session_id", &self.session_id)
208 .field(
209 "bytes_transferred",
210 &self.bytes_transferred.load(Ordering::Relaxed),
211 )
212 .field(
213 "blocks_requested",
214 &self.blocks_requested.load(Ordering::Relaxed),
215 )
216 .field(
217 "blocks_received",
218 &self.blocks_received.load(Ordering::Relaxed),
219 )
220 .field("blocks_failed", &self.blocks_failed.load(Ordering::Relaxed))
221 .field("total_retries", &self.total_retries.load(Ordering::Relaxed))
222 .field("started_at_ms", &self.started_at_ms)
223 .finish()
224 }
225}
226
227pub struct SessionMetricsStore {
236 sessions: RwLock<HashMap<String, Arc<SessionMetrics>>>,
237 max_sessions: usize,
238 completed: RwLock<Vec<SessionMetricsSnapshot>>,
239}
240
241impl SessionMetricsStore {
242 pub fn new(max_sessions: usize) -> Arc<Self> {
246 Arc::new(Self {
247 sessions: RwLock::new(HashMap::new()),
248 max_sessions,
249 completed: RwLock::new(Vec::new()),
250 })
251 }
252
253 pub fn create_session(
257 &self,
258 session_id: impl Into<String>,
259 started_at_ms: u64,
260 ) -> Arc<SessionMetrics> {
261 let id: String = session_id.into();
262 let metrics = SessionMetrics::new(id.clone(), started_at_ms);
263 self.sessions.write().insert(id, Arc::clone(&metrics));
264 metrics
265 }
266
267 pub fn get_session(&self, session_id: &str) -> Option<Arc<SessionMetrics>> {
269 self.sessions.read().get(session_id).cloned()
270 }
271
272 pub fn complete_session(&self, session_id: &str, now_ms: u64) -> bool {
277 let removed = self.sessions.write().remove(session_id);
278 match removed {
279 None => false,
280 Some(metrics) => {
281 let snap = metrics.snapshot(now_ms);
282 let mut completed = self.completed.write();
283 completed.push(snap);
284 while completed.len() > self.max_sessions {
286 completed.remove(0);
287 }
288 true
289 }
290 }
291 }
292
293 pub fn active_count(&self) -> usize {
295 self.sessions.read().len()
296 }
297
298 pub fn completed_snapshots(&self) -> Vec<SessionMetricsSnapshot> {
300 self.completed.read().clone()
301 }
302
303 pub fn total_bytes_transferred(&self) -> u64 {
305 self.sessions
306 .read()
307 .values()
308 .map(|m| m.bytes_transferred())
309 .sum()
310 }
311}
312
313impl std::fmt::Debug for SessionMetricsStore {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 f.debug_struct("SessionMetricsStore")
316 .field("active_count", &self.active_count())
317 .field("max_sessions", &self.max_sessions)
318 .field("completed_count", &self.completed.read().len())
319 .finish()
320 }
321}
322
323#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
332 fn test_default_config_values() {
333 let cfg = SessionConfig::default();
334 assert_eq!(cfg.timeout, Duration::from_secs(30));
335 assert_eq!(cfg.max_bytes, 1024 * 1024 * 1024);
336 assert_eq!(cfg.max_concurrent_requests, 16);
337 assert_eq!(cfg.request_timeout, Duration::from_secs(10));
338 assert_eq!(cfg.max_retries, 3);
339 assert_eq!(cfg.backoff_base, Duration::from_millis(200));
340 }
341
342 #[test]
343 fn test_high_throughput_config() {
344 let cfg = SessionConfig::high_throughput();
345 assert_eq!(cfg.timeout, Duration::from_secs(120));
346 assert_eq!(cfg.max_bytes, 10 * 1024 * 1024 * 1024);
347 assert_eq!(cfg.max_concurrent_requests, 64);
348 assert_eq!(cfg.request_timeout, Duration::from_secs(30));
349 assert_eq!(cfg.max_retries, 5);
350 assert_eq!(cfg.backoff_base, Duration::from_millis(100));
351 }
352
353 #[test]
354 fn test_low_latency_config() {
355 let cfg = SessionConfig::low_latency();
356 assert_eq!(cfg.timeout, Duration::from_secs(5));
357 assert_eq!(cfg.max_bytes, 100 * 1024 * 1024);
358 assert_eq!(cfg.max_concurrent_requests, 8);
359 assert_eq!(cfg.request_timeout, Duration::from_secs(2));
360 assert_eq!(cfg.max_retries, 1);
361 assert_eq!(cfg.backoff_base, Duration::from_millis(50));
362 }
363
364 #[test]
365 fn test_backoff_exponential() {
366 let cfg = SessionConfig::default(); assert_eq!(cfg.backoff_for_retry(0), Duration::from_millis(200)); assert_eq!(cfg.backoff_for_retry(1), Duration::from_millis(400)); assert_eq!(cfg.backoff_for_retry(2), Duration::from_millis(800)); assert_eq!(cfg.backoff_for_retry(3), Duration::from_millis(1_600));
371 assert_eq!(cfg.backoff_for_retry(4), Duration::from_millis(3_200));
372 assert_eq!(cfg.backoff_for_retry(5), Duration::from_millis(6_400));
373 assert_eq!(cfg.backoff_for_retry(6), Duration::from_millis(12_800));
375 assert_eq!(cfg.backoff_for_retry(7), Duration::from_millis(12_800));
376 assert_eq!(cfg.backoff_for_retry(100), Duration::from_millis(12_800));
377 }
378
379 #[test]
382 fn test_session_metrics_record_bytes() {
383 let m = SessionMetrics::new("sess-bytes", 0);
384 assert_eq!(m.bytes_transferred(), 0);
385 m.record_bytes(512);
386 m.record_bytes(1024);
387 assert_eq!(m.bytes_transferred(), 1_536);
388 }
389
390 #[test]
391 fn test_session_metrics_record_blocks() {
392 let m = SessionMetrics::new("sess-blocks", 0);
393 m.record_block_requested();
394 m.record_block_requested();
395 m.record_block_requested();
396 m.record_block_received();
397 m.record_block_received();
398 m.record_block_failed();
399 m.record_retry();
400 m.record_retry();
401
402 let snap = m.snapshot(1_000);
403 assert_eq!(snap.blocks_requested, 3);
404 assert_eq!(snap.blocks_received, 2);
405 assert_eq!(snap.blocks_failed, 1);
406 assert_eq!(snap.total_retries, 2);
407 assert_eq!(m.blocks_received(), 2);
408 }
409
410 #[test]
411 fn test_session_metrics_snapshot_throughput() {
412 let m = SessionMetrics::new("sess-tput", 0);
413 let mib: u64 = 1024 * 1024;
415 m.record_bytes(mib);
416 let snap = m.snapshot(2_000);
417 assert_eq!(snap.elapsed_ms, 2_000);
418 assert_eq!(snap.bytes_transferred, mib);
419 let expected = mib as f64 / 2.0;
421 let diff = (snap.throughput_bps - expected).abs();
422 assert!(
423 diff < 1.0,
424 "throughput mismatch: got {}, expected {}",
425 snap.throughput_bps,
426 expected
427 );
428 }
429
430 #[test]
433 fn test_session_metrics_store_create_and_get() {
434 let store = SessionMetricsStore::new(10);
435 assert_eq!(store.active_count(), 0);
436
437 let m = store.create_session("alpha", 1_000);
438 m.record_bytes(256);
439 assert_eq!(store.active_count(), 1);
440
441 let fetched = store.get_session("alpha").expect("session should exist");
442 assert_eq!(fetched.bytes_transferred(), 256);
443
444 assert!(store.get_session("missing").is_none());
445 }
446
447 #[test]
448 fn test_session_metrics_store_complete() {
449 let store = SessionMetricsStore::new(10);
450 store.create_session("beta", 0).record_bytes(4_096);
451
452 assert_eq!(store.active_count(), 1);
453 assert!(store.completed_snapshots().is_empty());
454
455 let found = store.complete_session("beta", 1_000);
456 assert!(found, "complete_session should return true");
457 assert_eq!(store.active_count(), 0);
458
459 let snaps = store.completed_snapshots();
460 assert_eq!(snaps.len(), 1);
461 assert_eq!(snaps[0].session_id, "beta");
462 assert_eq!(snaps[0].bytes_transferred, 4_096);
463
464 assert!(!store.complete_session("ghost", 2_000));
466 }
467
468 #[test]
469 fn test_session_metrics_store_total_bytes() {
470 let store = SessionMetricsStore::new(10);
471 store.create_session("s1", 0).record_bytes(100);
472 store.create_session("s2", 0).record_bytes(200);
473 store.create_session("s3", 0).record_bytes(300);
474
475 assert_eq!(store.total_bytes_transferred(), 600);
476
477 store.complete_session("s2", 1_000);
479 assert_eq!(store.total_bytes_transferred(), 400);
480 }
481
482 #[test]
483 fn test_max_sessions_cap() {
484 let store = SessionMetricsStore::new(3);
486
487 for i in 0..6u64 {
488 let id = format!("sess-{}", i);
489 store.create_session(&id, i * 100).record_bytes(i * 10);
490 store.complete_session(&id, (i + 1) * 100);
491 }
492
493 assert_eq!(store.active_count(), 0);
495
496 let snaps = store.completed_snapshots();
498 assert_eq!(snaps.len(), 3);
499
500 let ids: Vec<&str> = snaps.iter().map(|s| s.session_id.as_str()).collect();
502 assert!(ids.contains(&"sess-3"));
503 assert!(ids.contains(&"sess-4"));
504 assert!(ids.contains(&"sess-5"));
505
506 let extra = store.create_session("extra-1", 9_000);
508 extra.record_bytes(999);
509 assert_eq!(store.active_count(), 1);
510 assert_eq!(extra.bytes_transferred(), 999);
511 }
512}