1use std::collections::HashMap;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub enum SessionCapability {
9 BlockExchange,
11 VectorSearch,
13 TensorLogic,
15 GossipRelay,
17 Bootstrap,
19}
20
21fn fnv1a(data: &[u8]) -> u64 {
23 const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
24 const PRIME: u64 = 1_099_511_628_211;
25 let mut hash = OFFSET_BASIS;
26 for &byte in data {
27 hash ^= u64::from(byte);
28 hash = hash.wrapping_mul(PRIME);
29 }
30 hash
31}
32
33fn compute_token_id(peer_id: &str, nonce: u64) -> u64 {
35 let combined = format!("{}{}", peer_id, nonce);
36 fnv1a(combined.as_bytes())
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct SessionToken {
42 pub token_id: u64,
44 pub peer_id: String,
46 pub nonce: u64,
48 pub issued_at_secs: u64,
50 pub expires_at_secs: u64,
52}
53
54impl SessionToken {
55 pub fn is_expired(&self, now_secs: u64) -> bool {
57 now_secs >= self.expires_at_secs
58 }
59
60 pub fn remaining_secs(&self, now_secs: u64) -> u64 {
62 self.expires_at_secs.saturating_sub(now_secs)
63 }
64}
65
66#[derive(Clone, Debug)]
68pub struct PeerSession {
69 pub token: SessionToken,
71 pub capabilities: Vec<SessionCapability>,
73 pub bytes_sent: u64,
75 pub bytes_received: u64,
77 pub message_count: u64,
79}
80
81impl PeerSession {
82 pub fn has_capability(&self, cap: SessionCapability) -> bool {
84 self.capabilities.contains(&cap)
85 }
86}
87
88#[derive(Clone, Debug)]
90pub struct SessionManagerConfig {
91 pub session_ttl_secs: u64,
93 pub max_sessions: usize,
95 pub nonce_seed: u64,
97}
98
99impl Default for SessionManagerConfig {
100 fn default() -> Self {
101 Self {
102 session_ttl_secs: 3600,
103 max_sessions: 1000,
104 nonce_seed: 0,
105 }
106 }
107}
108
109pub struct PeerSessionManager {
111 sessions: HashMap<u64, PeerSession>,
113 config: SessionManagerConfig,
115 next_nonce: u64,
117}
118
119impl PeerSessionManager {
120 pub fn new(config: SessionManagerConfig) -> Self {
122 let next_nonce = config.nonce_seed;
123 Self {
124 sessions: HashMap::new(),
125 config,
126 next_nonce,
127 }
128 }
129
130 pub fn open_session(
135 &mut self,
136 peer_id: String,
137 capabilities: Vec<SessionCapability>,
138 now_secs: u64,
139 ) -> Result<u64, String> {
140 if self.sessions.len() >= self.config.max_sessions {
141 return Err("session limit reached".to_string());
142 }
143
144 let nonce = self.next_nonce;
145 self.next_nonce = self.next_nonce.wrapping_add(1);
146
147 let token_id = compute_token_id(&peer_id, nonce);
148 let expires_at_secs = now_secs.saturating_add(self.config.session_ttl_secs);
149
150 let token = SessionToken {
151 token_id,
152 peer_id,
153 nonce,
154 issued_at_secs: now_secs,
155 expires_at_secs,
156 };
157
158 let session = PeerSession {
159 token,
160 capabilities,
161 bytes_sent: 0,
162 bytes_received: 0,
163 message_count: 0,
164 };
165
166 self.sessions.insert(token_id, session);
167 Ok(token_id)
168 }
169
170 pub fn get_session(&self, token_id: u64) -> Option<&PeerSession> {
172 self.sessions.get(&token_id)
173 }
174
175 pub fn record_traffic(&mut self, token_id: u64, sent: u64, received: u64) {
178 if let Some(session) = self.sessions.get_mut(&token_id) {
179 session.bytes_sent = session.bytes_sent.saturating_add(sent);
180 session.bytes_received = session.bytes_received.saturating_add(received);
181 session.message_count = session.message_count.saturating_add(1);
182 }
183 }
184
185 pub fn close_session(&mut self, token_id: u64) -> bool {
187 self.sessions.remove(&token_id).is_some()
188 }
189
190 pub fn evict_expired(&mut self, now_secs: u64) -> usize {
193 let before = self.sessions.len();
194 self.sessions
195 .retain(|_, session| !session.token.is_expired(now_secs));
196 before - self.sessions.len()
197 }
198
199 pub fn active_sessions(&self, now_secs: u64) -> Vec<&PeerSession> {
201 self.sessions
202 .values()
203 .filter(|s| !s.token.is_expired(now_secs))
204 .collect()
205 }
206
207 pub fn sessions_with_capability(
209 &self,
210 cap: SessionCapability,
211 now_secs: u64,
212 ) -> Vec<&PeerSession> {
213 self.sessions
214 .values()
215 .filter(|s| !s.token.is_expired(now_secs) && s.has_capability(cap))
216 .collect()
217 }
218}
219
220#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn default_manager() -> PeerSessionManager {
229 PeerSessionManager::new(SessionManagerConfig::default())
230 }
231
232 fn now() -> u64 {
233 1_700_000_000_u64
234 }
235
236 #[test]
239 fn open_session_returns_token_id() {
240 let mut mgr = default_manager();
241 let result = mgr.open_session(
242 "peer-alpha".to_string(),
243 vec![SessionCapability::BlockExchange],
244 now(),
245 );
246 assert!(result.is_ok());
247 }
248
249 #[test]
252 fn token_id_is_correct_fnv1a_hash() {
253 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
254 nonce_seed: 42,
255 ..Default::default()
256 });
257 let token_id = mgr
258 .open_session("peer-x".to_string(), vec![], now())
259 .expect("open_session failed");
260 let expected = compute_token_id("peer-x", 42);
261 assert_eq!(token_id, expected);
262 }
263
264 #[test]
267 fn get_session_returns_some_for_existing() {
268 let mut mgr = default_manager();
269 let tid = mgr
270 .open_session("peer-a".to_string(), vec![], now())
271 .expect("open_session failed");
272 assert!(mgr.get_session(tid).is_some());
273 }
274
275 #[test]
278 fn get_session_returns_none_for_unknown() {
279 let mgr = default_manager();
280 assert!(mgr.get_session(9999).is_none());
281 }
282
283 #[test]
286 fn open_session_errors_when_at_limit() {
287 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
288 max_sessions: 2,
289 ..Default::default()
290 });
291 mgr.open_session("p1".to_string(), vec![], now())
292 .expect("first session");
293 mgr.open_session("p2".to_string(), vec![], now())
294 .expect("second session");
295 let err = mgr.open_session("p3".to_string(), vec![], now());
296 assert_eq!(err, Err("session limit reached".to_string()));
297 }
298
299 #[test]
302 fn has_capability_returns_true_when_present() {
303 let mut mgr = default_manager();
304 let tid = mgr
305 .open_session(
306 "peer-b".to_string(),
307 vec![
308 SessionCapability::VectorSearch,
309 SessionCapability::Bootstrap,
310 ],
311 now(),
312 )
313 .expect("open_session failed");
314 let session = mgr.get_session(tid).expect("session not found");
315 assert!(session.has_capability(SessionCapability::VectorSearch));
316 assert!(session.has_capability(SessionCapability::Bootstrap));
317 }
318
319 #[test]
322 fn has_capability_returns_false_when_absent() {
323 let mut mgr = default_manager();
324 let tid = mgr
325 .open_session(
326 "peer-c".to_string(),
327 vec![SessionCapability::BlockExchange],
328 now(),
329 )
330 .expect("open_session failed");
331 let session = mgr.get_session(tid).expect("session not found");
332 assert!(!session.has_capability(SessionCapability::TensorLogic));
333 }
334
335 #[test]
338 fn record_traffic_updates_bytes_and_count() {
339 let mut mgr = default_manager();
340 let tid = mgr
341 .open_session("peer-d".to_string(), vec![], now())
342 .expect("open_session failed");
343 mgr.record_traffic(tid, 100, 200);
344 mgr.record_traffic(tid, 50, 75);
345 let session = mgr.get_session(tid).expect("session not found");
346 assert_eq!(session.bytes_sent, 150);
347 assert_eq!(session.bytes_received, 275);
348 assert_eq!(session.message_count, 2);
349 }
350
351 #[test]
354 fn record_traffic_unknown_id_is_noop() {
355 let mut mgr = default_manager();
356 mgr.record_traffic(0xdeadbeef, 1, 1);
358 }
359
360 #[test]
363 fn close_session_removes_session() {
364 let mut mgr = default_manager();
365 let tid = mgr
366 .open_session("peer-e".to_string(), vec![], now())
367 .expect("open_session failed");
368 assert!(mgr.close_session(tid));
369 assert!(mgr.get_session(tid).is_none());
370 }
371
372 #[test]
375 fn close_session_returns_false_for_unknown() {
376 let mut mgr = default_manager();
377 assert!(!mgr.close_session(0xdeadbeef));
378 }
379
380 #[test]
383 fn evict_expired_removes_only_expired() {
384 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
385 session_ttl_secs: 100,
386 ..Default::default()
387 });
388 let t0 = 1_000_u64;
389 let tid_a = mgr
391 .open_session("peer-f".to_string(), vec![], t0)
392 .expect("open a");
393 let tid_b = mgr
394 .open_session("peer-g".to_string(), vec![], t0)
395 .expect("open b");
396
397 let t1 = t0 + 200;
399 let tid_c = mgr
401 .open_session("peer-h".to_string(), vec![], t1)
402 .expect("open c");
403
404 let removed = mgr.evict_expired(t1);
405 assert_eq!(removed, 2);
406 assert!(mgr.get_session(tid_a).is_none());
407 assert!(mgr.get_session(tid_b).is_none());
408 assert!(mgr.get_session(tid_c).is_some());
409 }
410
411 #[test]
414 fn evict_expired_returns_zero_when_nothing_expired() {
415 let mut mgr = default_manager();
416 mgr.open_session("peer-i".to_string(), vec![], now())
417 .expect("open");
418 let removed = mgr.evict_expired(now());
419 assert_eq!(removed, 0);
420 }
421
422 #[test]
425 fn active_sessions_counts_correctly() {
426 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
427 session_ttl_secs: 50,
428 ..Default::default()
429 });
430 let t0 = 1_000_u64;
431 mgr.open_session("p1".to_string(), vec![], t0).expect("p1");
432 mgr.open_session("p2".to_string(), vec![], t0).expect("p2");
433
434 assert_eq!(mgr.active_sessions(t0 + 30).len(), 2);
436
437 assert_eq!(mgr.active_sessions(t0 + 60).len(), 0);
439 }
440
441 #[test]
444 fn sessions_with_capability_filters_correctly() {
445 let mut mgr = default_manager();
446 let t = now();
447 mgr.open_session(
448 "p1".to_string(),
449 vec![SessionCapability::GossipRelay, SessionCapability::Bootstrap],
450 t,
451 )
452 .expect("p1");
453 mgr.open_session("p2".to_string(), vec![SessionCapability::BlockExchange], t)
454 .expect("p2");
455 mgr.open_session("p3".to_string(), vec![SessionCapability::GossipRelay], t)
456 .expect("p3");
457
458 let gossip_peers = mgr.sessions_with_capability(SessionCapability::GossipRelay, t);
459 assert_eq!(gossip_peers.len(), 2);
460
461 let block_peers = mgr.sessions_with_capability(SessionCapability::BlockExchange, t);
462 assert_eq!(block_peers.len(), 1);
463
464 let tensor_peers = mgr.sessions_with_capability(SessionCapability::TensorLogic, t);
465 assert_eq!(tensor_peers.len(), 0);
466 }
467
468 #[test]
471 fn token_is_expired_and_remaining_secs() {
472 let token = SessionToken {
473 token_id: 1,
474 peer_id: "p".to_string(),
475 nonce: 0,
476 issued_at_secs: 1000,
477 expires_at_secs: 1100,
478 };
479
480 assert!(!token.is_expired(1099));
481 assert!(token.is_expired(1100));
482 assert!(token.is_expired(1200));
483
484 assert_eq!(token.remaining_secs(1000), 100);
485 assert_eq!(token.remaining_secs(1099), 1);
486 assert_eq!(token.remaining_secs(1100), 0);
487 assert_eq!(token.remaining_secs(1200), 0);
489 }
490
491 #[test]
494 fn sessions_with_capability_excludes_expired() {
495 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
496 session_ttl_secs: 10,
497 ..Default::default()
498 });
499 let t0 = 500_u64;
500 mgr.open_session(
501 "old-peer".to_string(),
502 vec![SessionCapability::VectorSearch],
503 t0,
504 )
505 .expect("old peer");
506
507 let t1 = t0 + 20;
509 mgr.open_session(
510 "new-peer".to_string(),
511 vec![SessionCapability::VectorSearch],
512 t1,
513 )
514 .expect("new peer");
515
516 let results = mgr.sessions_with_capability(SessionCapability::VectorSearch, t1);
517 assert_eq!(results.len(), 1);
518 assert_eq!(results[0].token.peer_id, "new-peer");
519 }
520
521 #[test]
524 fn nonce_increments_per_session() {
525 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
526 nonce_seed: 10,
527 ..Default::default()
528 });
529 let t = now();
530 let tid1 = mgr.open_session("pa".to_string(), vec![], t).expect("s1");
531 let tid2 = mgr.open_session("pa".to_string(), vec![], t).expect("s2");
532
533 assert_ne!(tid1, tid2);
535
536 let s1 = mgr.get_session(tid1).expect("s1 not found");
537 let s2 = mgr.get_session(tid2).expect("s2 not found");
538 assert_eq!(s1.token.nonce, 10);
539 assert_eq!(s2.token.nonce, 11);
540 }
541
542 #[test]
545 fn token_timestamps_are_set_correctly() {
546 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
547 session_ttl_secs: 7200,
548 ..Default::default()
549 });
550 let t = now();
551 let tid = mgr
552 .open_session("peer-ts".to_string(), vec![], t)
553 .expect("open");
554 let session = mgr.get_session(tid).expect("get");
555 assert_eq!(session.token.issued_at_secs, t);
556 assert_eq!(session.token.expires_at_secs, t + 7200);
557 }
558
559 #[test]
562 fn close_session_frees_slot_for_reuse() {
563 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
564 max_sessions: 1,
565 ..Default::default()
566 });
567 let t = now();
568 let tid = mgr.open_session("p1".to_string(), vec![], t).expect("p1");
569 assert!(mgr.open_session("p2".to_string(), vec![], t).is_err());
571 mgr.close_session(tid);
573 assert!(mgr.open_session("p2".to_string(), vec![], t).is_ok());
575 }
576}