1use parking_lot::RwLock;
50use std::collections::HashMap;
51use std::fmt;
52use std::sync::atomic::{AtomicU64, Ordering};
53use std::sync::Arc;
54use std::time::{Duration, Instant};
55use thiserror::Error;
56
57pub const MAX_CONCURRENT_SESSIONS: usize = 256;
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub struct PeerId(pub String);
70
71impl PeerId {
72 pub fn new(s: impl Into<String>) -> Self {
74 Self(s.into())
75 }
76}
77
78impl fmt::Display for PeerId {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.write_str(&self.0)
81 }
82}
83
84impl From<String> for PeerId {
85 fn from(s: String) -> Self {
86 Self(s)
87 }
88}
89
90impl From<&str> for PeerId {
91 fn from(s: &str) -> Self {
92 Self(s.to_string())
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub struct SessionId([u8; 16]);
103
104impl SessionId {
105 pub fn new() -> Result<Self, SessionError> {
112 let mut buf = [0u8; 16];
113 getrandom::fill(&mut buf).map_err(|e| SessionError::IdGeneration(e.to_string()))?;
114 Ok(Self(buf))
115 }
116
117 pub fn from_bytes(bytes: [u8; 16]) -> Self {
119 Self(bytes)
120 }
121
122 pub fn as_bytes(&self) -> &[u8; 16] {
124 &self.0
125 }
126}
127
128impl fmt::Display for SessionId {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 for byte in &self.0 {
131 write!(f, "{byte:02x}")?;
132 }
133 Ok(())
134 }
135}
136
137#[derive(Debug, Clone)]
141pub enum SessionStatus {
142 Pending,
144
145 Running {
147 started_at: Instant,
149 },
150
151 Completed {
153 result_cid: Option<String>,
155 },
156
157 Failed {
159 reason: String,
161 },
162
163 Cancelled,
165}
166
167#[derive(Debug)]
171struct SessionRecord {
172 goal: String,
174
175 peers: Vec<PeerId>,
177
178 status: SessionStatus,
180
181 created_at: Instant,
183}
184
185#[derive(Debug, Error)]
189pub enum SessionError {
190 #[error("session id generation failed: {0}")]
192 IdGeneration(String),
193
194 #[error("session not found")]
196 NotFound,
197
198 #[error("invalid state transition: {0}")]
200 InvalidTransition(String),
201
202 #[error("capacity exceeded: maximum {MAX_CONCURRENT_SESSIONS} concurrent sessions")]
204 CapacityExceeded,
205}
206
207#[derive(Debug)]
214pub struct SessionMetrics {
215 pub total_created: AtomicU64,
217
218 pub total_completed: AtomicU64,
220
221 pub total_failed: AtomicU64,
223
224 pub total_cancelled: AtomicU64,
226}
227
228impl SessionMetrics {
229 fn new() -> Self {
231 Self {
232 total_created: AtomicU64::new(0),
233 total_completed: AtomicU64::new(0),
234 total_failed: AtomicU64::new(0),
235 total_cancelled: AtomicU64::new(0),
236 }
237 }
238
239 pub fn snapshot(&self) -> SessionMetricsSnapshot {
244 SessionMetricsSnapshot {
245 total_created: self.total_created.load(Ordering::Relaxed),
246 total_completed: self.total_completed.load(Ordering::Relaxed),
247 total_failed: self.total_failed.load(Ordering::Relaxed),
248 total_cancelled: self.total_cancelled.load(Ordering::Relaxed),
249 }
250 }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub struct SessionMetricsSnapshot {
258 pub total_created: u64,
260
261 pub total_completed: u64,
263
264 pub total_failed: u64,
266
267 pub total_cancelled: u64,
269}
270
271#[derive(Debug, Clone)]
284pub struct DistributedSessionManager {
285 inner: Arc<ManagerInner>,
286}
287
288#[derive(Debug)]
289struct ManagerInner {
290 registry: RwLock<HashMap<SessionId, SessionRecord>>,
291 metrics: SessionMetrics,
292}
293
294impl DistributedSessionManager {
295 pub fn new() -> Self {
297 Self {
298 inner: Arc::new(ManagerInner {
299 registry: RwLock::new(HashMap::new()),
300 metrics: SessionMetrics::new(),
301 }),
302 }
303 }
304
305 pub fn create_session(
315 &self,
316 goal: &str,
317 peers: Vec<PeerId>,
318 ) -> Result<SessionId, SessionError> {
319 {
321 let guard = self.inner.registry.read();
322 if guard.len() >= MAX_CONCURRENT_SESSIONS {
323 return Err(SessionError::CapacityExceeded);
324 }
325 }
326
327 let id = SessionId::new()?;
328
329 let record = SessionRecord {
330 goal: goal.to_string(),
331 peers,
332 status: SessionStatus::Pending,
333 created_at: Instant::now(),
334 };
335
336 let mut guard = self.inner.registry.write();
338 if guard.len() >= MAX_CONCURRENT_SESSIONS {
339 return Err(SessionError::CapacityExceeded);
340 }
341 guard.insert(id, record);
342 self.inner
343 .metrics
344 .total_created
345 .fetch_add(1, Ordering::Relaxed);
346
347 Ok(id)
348 }
349
350 pub fn set_running(&self, id: &SessionId) -> Result<(), SessionError> {
360 let mut guard = self.inner.registry.write();
361 let record = guard.get_mut(id).ok_or(SessionError::NotFound)?;
362 match &record.status {
363 SessionStatus::Pending => {
364 record.status = SessionStatus::Running {
365 started_at: Instant::now(),
366 };
367 Ok(())
368 }
369 other => Err(SessionError::InvalidTransition(format!(
370 "expected Pending, got {other:?}"
371 ))),
372 }
373 }
374
375 pub fn set_completed(
385 &self,
386 id: &SessionId,
387 result_cid: Option<String>,
388 ) -> Result<(), SessionError> {
389 let mut guard = self.inner.registry.write();
390 let record = guard.get_mut(id).ok_or(SessionError::NotFound)?;
391 match &record.status {
392 SessionStatus::Pending | SessionStatus::Running { .. } => {
393 record.status = SessionStatus::Completed { result_cid };
394 self.inner
395 .metrics
396 .total_completed
397 .fetch_add(1, Ordering::Relaxed);
398 Ok(())
399 }
400 other => Err(SessionError::InvalidTransition(format!(
401 "cannot complete from {other:?}"
402 ))),
403 }
404 }
405
406 pub fn set_failed(
415 &self,
416 id: &SessionId,
417 reason: impl Into<String>,
418 ) -> Result<(), SessionError> {
419 let mut guard = self.inner.registry.write();
420 let record = guard.get_mut(id).ok_or(SessionError::NotFound)?;
421 match &record.status {
422 SessionStatus::Pending | SessionStatus::Running { .. } => {
423 record.status = SessionStatus::Failed {
424 reason: reason.into(),
425 };
426 self.inner
427 .metrics
428 .total_failed
429 .fetch_add(1, Ordering::Relaxed);
430 Ok(())
431 }
432 other => Err(SessionError::InvalidTransition(format!(
433 "cannot fail from {other:?}"
434 ))),
435 }
436 }
437
438 pub fn cancel_session(&self, id: &SessionId) -> Result<(), SessionError> {
449 let mut guard = self.inner.registry.write();
450 let record = guard.get_mut(id).ok_or(SessionError::NotFound)?;
451 match &record.status {
452 SessionStatus::Pending | SessionStatus::Running { .. } => {
453 record.status = SessionStatus::Cancelled;
454 self.inner
455 .metrics
456 .total_cancelled
457 .fetch_add(1, Ordering::Relaxed);
458 Ok(())
459 }
460 other => Err(SessionError::InvalidTransition(format!(
461 "cannot cancel a session in state {other:?}"
462 ))),
463 }
464 }
465
466 pub fn session_status(&self, id: &SessionId) -> Option<SessionStatus> {
470 let guard = self.inner.registry.read();
471 guard.get(id).map(|r| r.status.clone())
472 }
473
474 pub fn session_goal(&self, id: &SessionId) -> Option<String> {
476 let guard = self.inner.registry.read();
477 guard.get(id).map(|r| r.goal.clone())
478 }
479
480 pub fn session_peers(&self, id: &SessionId) -> Option<Vec<PeerId>> {
482 let guard = self.inner.registry.read();
483 guard.get(id).map(|r| r.peers.clone())
484 }
485
486 pub fn active_count(&self) -> usize {
491 self.inner.registry.read().len()
492 }
493
494 pub fn gc_expired_sessions(&self, max_age: Duration) -> usize {
502 let now = Instant::now();
503 let mut guard = self.inner.registry.write();
504 let before = guard.len();
505 guard.retain(|_id, record| now.duration_since(record.created_at) <= max_age);
506 before - guard.len()
507 }
508
509 pub fn metrics(&self) -> &SessionMetrics {
515 &self.inner.metrics
516 }
517}
518
519impl Default for DistributedSessionManager {
520 fn default() -> Self {
521 Self::new()
522 }
523}
524
525#[cfg(test)]
528mod tests {
529 use super::*;
530 use std::time::Duration;
531
532 fn peers(names: &[&str]) -> Vec<PeerId> {
533 names.iter().map(|s| PeerId::new(*s)).collect()
534 }
535
536 #[test]
539 fn test_session_id_display_is_32_hex_chars() {
540 let id = SessionId::new().expect("getrandom must succeed in tests");
541 let s = id.to_string();
542 assert_eq!(s.len(), 32, "SessionId display must be 32 hex chars");
543 assert!(
544 s.chars().all(|c| c.is_ascii_hexdigit()),
545 "All chars must be hex digits"
546 );
547 assert_eq!(s, s.to_lowercase(), "Hex must be lowercase");
549 }
550
551 #[test]
552 fn test_session_id_from_bytes_roundtrip() {
553 let bytes = [
554 0xde, 0xad, 0xbe, 0xef, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99,
555 0xaa, 0xbb,
556 ];
557 let id = SessionId::from_bytes(bytes);
558 assert_eq!(id.as_bytes(), &bytes);
559 assert_eq!(id.to_string(), "deadbeef00112233445566778899aabb");
560 }
561
562 #[test]
565 fn test_create_session_returns_pending() {
566 let mgr = DistributedSessionManager::new();
567 let id = mgr
568 .create_session("parent(X, Y)", peers(&["p1"]))
569 .expect("create must succeed");
570
571 assert!(
572 matches!(mgr.session_status(&id), Some(SessionStatus::Pending)),
573 "Newly created session must be Pending"
574 );
575 }
576
577 #[test]
578 fn test_create_session_increments_active_count() {
579 let mgr = DistributedSessionManager::new();
580 assert_eq!(mgr.active_count(), 0);
581 let _ = mgr
582 .create_session("goal_a", peers(&[]))
583 .expect("create must succeed");
584 assert_eq!(mgr.active_count(), 1);
585 let _ = mgr
586 .create_session("goal_b", peers(&[]))
587 .expect("create must succeed");
588 assert_eq!(mgr.active_count(), 2);
589 }
590
591 #[test]
594 fn test_set_running_transitions_from_pending() {
595 let mgr = DistributedSessionManager::new();
596 let id = mgr
597 .create_session("foo", peers(&[]))
598 .expect("test: should succeed");
599 mgr.set_running(&id).expect("set_running must succeed");
600 assert!(matches!(
601 mgr.session_status(&id),
602 Some(SessionStatus::Running { .. })
603 ));
604 }
605
606 #[test]
607 fn test_set_running_fails_when_not_pending() {
608 let mgr = DistributedSessionManager::new();
609 let id = mgr
610 .create_session("foo", peers(&[]))
611 .expect("test: should succeed");
612 mgr.set_running(&id).expect("test: should succeed");
613 let result = mgr.set_running(&id);
615 assert!(
616 matches!(result, Err(SessionError::InvalidTransition(_))),
617 "set_running on Running state must fail"
618 );
619 }
620
621 #[test]
622 fn test_set_completed_from_running() {
623 let mgr = DistributedSessionManager::new();
624 let id = mgr
625 .create_session("foo", peers(&[]))
626 .expect("test: should succeed");
627 mgr.set_running(&id).expect("test: should succeed");
628 mgr.set_completed(&id, Some("bafybei...".to_string()))
629 .expect("complete must succeed");
630 assert!(matches!(
631 mgr.session_status(&id),
632 Some(SessionStatus::Completed { .. })
633 ));
634 }
635
636 #[test]
637 fn test_set_failed_from_running() {
638 let mgr = DistributedSessionManager::new();
639 let id = mgr
640 .create_session("foo", peers(&[]))
641 .expect("test: should succeed");
642 mgr.set_running(&id).expect("test: should succeed");
643 mgr.set_failed(&id, "peer timed out")
644 .expect("fail must succeed");
645 assert!(matches!(
646 mgr.session_status(&id),
647 Some(SessionStatus::Failed { .. })
648 ));
649 }
650
651 #[test]
654 fn test_cancel_pending_session() {
655 let mgr = DistributedSessionManager::new();
656 let id = mgr
657 .create_session("foo", peers(&[]))
658 .expect("test: should succeed");
659 mgr.cancel_session(&id).expect("cancel must succeed");
660 assert!(matches!(
661 mgr.session_status(&id),
662 Some(SessionStatus::Cancelled)
663 ));
664 }
665
666 #[test]
667 fn test_cancel_running_session() {
668 let mgr = DistributedSessionManager::new();
669 let id = mgr
670 .create_session("foo", peers(&[]))
671 .expect("test: should succeed");
672 mgr.set_running(&id).expect("test: should succeed");
673 mgr.cancel_session(&id)
674 .expect("cancel of running must succeed");
675 assert!(matches!(
676 mgr.session_status(&id),
677 Some(SessionStatus::Cancelled)
678 ));
679 }
680
681 #[test]
682 fn test_cancel_completed_session_fails() {
683 let mgr = DistributedSessionManager::new();
684 let id = mgr
685 .create_session("foo", peers(&[]))
686 .expect("test: should succeed");
687 mgr.set_completed(&id, None).expect("test: should succeed");
688 let result = mgr.cancel_session(&id);
689 assert!(
690 matches!(result, Err(SessionError::InvalidTransition(_))),
691 "Cancelling a Completed session must fail"
692 );
693 }
694
695 #[test]
696 fn test_cancel_nonexistent_session_returns_not_found() {
697 let mgr = DistributedSessionManager::new();
698 let id = SessionId::from_bytes([0u8; 16]);
699 let result = mgr.cancel_session(&id);
700 assert!(matches!(result, Err(SessionError::NotFound)));
701 }
702
703 #[test]
706 fn test_gc_removes_old_sessions() {
707 let mgr = DistributedSessionManager::new();
708 let _id = mgr
709 .create_session("old", peers(&[]))
710 .expect("test: should succeed");
711 assert_eq!(mgr.active_count(), 1);
712
713 let removed = mgr.gc_expired_sessions(Duration::ZERO);
715 assert_eq!(removed, 1, "GC must remove the old session");
716 assert_eq!(mgr.active_count(), 0);
717 }
718
719 #[test]
720 fn test_gc_preserves_fresh_sessions() {
721 let mgr = DistributedSessionManager::new();
722 let _ = mgr
723 .create_session("fresh", peers(&[]))
724 .expect("test: should succeed");
725 assert_eq!(mgr.active_count(), 1);
726
727 let removed = mgr.gc_expired_sessions(Duration::from_secs(3600));
729 assert_eq!(
730 removed, 0,
731 "No sessions should be GCed with a 1-hour window"
732 );
733 assert_eq!(mgr.active_count(), 1);
734 }
735
736 #[test]
739 fn test_capacity_exceeded_returns_error() {
740 let mgr = DistributedSessionManager::new();
741
742 for i in 0..MAX_CONCURRENT_SESSIONS {
743 mgr.create_session(&format!("goal_{i}"), peers(&[]))
744 .unwrap_or_else(|e| panic!("create #{i} must succeed: {e}"));
745 }
746
747 let result = mgr.create_session("one_too_many", peers(&[]));
749 assert!(
750 matches!(result, Err(SessionError::CapacityExceeded)),
751 "Must return CapacityExceeded when limit reached"
752 );
753 }
754
755 #[test]
758 fn test_metrics_count_created() {
759 let mgr = DistributedSessionManager::new();
760 let snap_before = mgr.metrics().snapshot();
761
762 let _ = mgr
763 .create_session("a", peers(&[]))
764 .expect("test: should succeed");
765 let _ = mgr
766 .create_session("b", peers(&[]))
767 .expect("test: should succeed");
768
769 let snap_after = mgr.metrics().snapshot();
770 assert_eq!(
771 snap_after.total_created - snap_before.total_created,
772 2,
773 "Two sessions must be counted as created"
774 );
775 }
776
777 #[test]
778 fn test_metrics_count_completed_failed_cancelled() {
779 let mgr = DistributedSessionManager::new();
780
781 let id1 = mgr
782 .create_session("complete_me", peers(&[]))
783 .expect("test: should succeed");
784 let id2 = mgr
785 .create_session("fail_me", peers(&[]))
786 .expect("test: should succeed");
787 let id3 = mgr
788 .create_session("cancel_me", peers(&[]))
789 .expect("test: should succeed");
790
791 mgr.set_completed(&id1, None).expect("test: should succeed");
792 mgr.set_failed(&id2, "oops").expect("test: should succeed");
793 mgr.cancel_session(&id3).expect("test: should succeed");
794
795 let snap = mgr.metrics().snapshot();
796 assert_eq!(snap.total_completed, 1);
797 assert_eq!(snap.total_failed, 1);
798 assert_eq!(snap.total_cancelled, 1);
799 }
800
801 #[test]
802 fn test_metrics_snapshot_is_plain_values() {
803 let mgr = DistributedSessionManager::new();
805 let _ = mgr
806 .create_session("x", peers(&[]))
807 .expect("test: should succeed");
808 let snap: SessionMetricsSnapshot = mgr.metrics().snapshot();
809 let _snap2 = snap; assert_eq!(snap.total_created, 1);
811 }
812
813 #[test]
816 fn test_session_goal_and_peers_accessible() {
817 let mgr = DistributedSessionManager::new();
818 let goal = "ancestor(alice, bob)";
819 let p = peers(&["peer-alpha", "peer-beta"]);
820 let id = mgr
821 .create_session(goal, p.clone())
822 .expect("test: should succeed");
823
824 assert_eq!(mgr.session_goal(&id).as_deref(), Some(goal));
825 assert_eq!(mgr.session_peers(&id), Some(p));
826 }
827
828 #[test]
831 fn test_status_of_unknown_session_returns_none() {
832 let mgr = DistributedSessionManager::new();
833 let id = SessionId::from_bytes([0xff; 16]);
834 assert!(mgr.session_status(&id).is_none());
835 }
836
837 #[test]
840 fn test_cloned_manager_shares_registry() {
841 let mgr1 = DistributedSessionManager::new();
842 let mgr2 = mgr1.clone();
843
844 let id = mgr1
845 .create_session("shared", peers(&[]))
846 .expect("create must succeed");
847
848 assert!(
850 mgr2.session_status(&id).is_some(),
851 "Cloned manager must share the same session registry"
852 );
853 }
854}