use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SyncPhase {
Handshake,
Discovery,
Transfer,
Verification,
Complete,
Failed { reason: String },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SyncDirection {
Push,
Pull,
Bidirectional,
}
#[derive(Clone, Debug)]
pub struct SyncSession {
pub session_id: u64,
pub peer_id: String,
pub direction: SyncDirection,
pub phase: SyncPhase,
pub have_cids: Vec<String>,
pub want_cids: Vec<String>,
pub transferred_bytes: u64,
pub started_at_tick: u64,
}
impl SyncSession {
pub fn is_terminal(&self) -> bool {
matches!(self.phase, SyncPhase::Complete | SyncPhase::Failed { .. })
}
pub fn pending_count(&self) -> usize {
self.want_cids.len()
}
}
#[derive(Clone, Debug, Default)]
pub struct SyncStats {
pub total_sessions: usize,
pub active_sessions: usize,
pub completed_sessions: usize,
pub failed_sessions: usize,
pub total_transferred_bytes: u64,
}
#[derive(Debug, Default)]
pub struct PeerSyncCoordinator {
pub sessions: HashMap<u64, SyncSession>,
pub next_session_id: u64,
}
impl PeerSyncCoordinator {
pub fn new() -> Self {
Self {
sessions: HashMap::new(),
next_session_id: 0,
}
}
pub fn start_session(&mut self, peer_id: &str, direction: SyncDirection, tick: u64) -> u64 {
let session_id = self.next_session_id;
self.next_session_id += 1;
let session = SyncSession {
session_id,
peer_id: peer_id.to_owned(),
direction,
phase: SyncPhase::Handshake,
have_cids: Vec::new(),
want_cids: Vec::new(),
transferred_bytes: 0,
started_at_tick: tick,
};
self.sessions.insert(session_id, session);
session_id
}
pub fn advance_phase(&mut self, session_id: u64, next_phase: SyncPhase) -> bool {
match self.sessions.get_mut(&session_id) {
Some(session) if !session.is_terminal() => {
session.phase = next_phase;
true
}
_ => false,
}
}
pub fn add_want(&mut self, session_id: u64, cid: &str) -> bool {
match self.sessions.get_mut(&session_id) {
Some(session) if !session.is_terminal() => {
session.want_cids.push(cid.to_owned());
true
}
_ => false,
}
}
pub fn mark_received(&mut self, session_id: u64, cid: &str, bytes: u64) -> bool {
match self.sessions.get_mut(&session_id) {
Some(session) => {
session.want_cids.retain(|c| c != cid);
if !session.have_cids.contains(&cid.to_owned()) {
session.have_cids.push(cid.to_owned());
}
session.transferred_bytes += bytes;
true
}
None => false,
}
}
pub fn fail_session(&mut self, session_id: u64, reason: String) -> bool {
match self.sessions.get_mut(&session_id) {
Some(session) => {
session.phase = SyncPhase::Failed { reason };
true
}
None => false,
}
}
pub fn complete_session(&mut self, session_id: u64) -> bool {
match self.sessions.get_mut(&session_id) {
Some(session) => {
session.phase = SyncPhase::Complete;
true
}
None => false,
}
}
pub fn active_sessions(&self) -> Vec<&SyncSession> {
let mut active: Vec<&SyncSession> = self
.sessions
.values()
.filter(|s| !s.is_terminal())
.collect();
active.sort_by_key(|s| s.session_id);
active
}
pub fn session(&self, session_id: u64) -> Option<&SyncSession> {
self.sessions.get(&session_id)
}
pub fn stats(&self) -> SyncStats {
let total_sessions = self.sessions.len();
let mut active_sessions: usize = 0;
let mut completed_sessions: usize = 0;
let mut failed_sessions: usize = 0;
let mut total_transferred_bytes: u64 = 0;
for session in self.sessions.values() {
total_transferred_bytes += session.transferred_bytes;
match &session.phase {
SyncPhase::Complete => completed_sessions += 1,
SyncPhase::Failed { .. } => failed_sessions += 1,
_ => active_sessions += 1,
}
}
SyncStats {
total_sessions,
active_sessions,
completed_sessions,
failed_sessions,
total_transferred_bytes,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn coordinator() -> PeerSyncCoordinator {
PeerSyncCoordinator::new()
}
#[test]
fn test_new_starts_empty() {
let c = coordinator();
assert!(c.sessions.is_empty());
assert_eq!(c.next_session_id, 0);
}
#[test]
fn test_start_session_creates_in_handshake() {
let mut c = coordinator();
let id = c.start_session("peer-a", SyncDirection::Pull, 10);
let s = c.session(id).expect("session must exist");
assert_eq!(s.phase, SyncPhase::Handshake);
assert_eq!(s.peer_id, "peer-a");
assert_eq!(s.started_at_tick, 10);
}
#[test]
fn test_start_session_increments_id() {
let mut c = coordinator();
let id0 = c.start_session("peer-a", SyncDirection::Push, 0);
let id1 = c.start_session("peer-b", SyncDirection::Pull, 1);
assert_eq!(id1, id0 + 1);
}
#[test]
fn test_advance_phase_succeeds() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Bidirectional, 0);
assert!(c.advance_phase(id, SyncPhase::Discovery));
assert_eq!(
c.session(id)
.expect("test: session must exist after advance_phase")
.phase,
SyncPhase::Discovery
);
}
#[test]
fn test_advance_phase_unknown_session() {
let mut c = coordinator();
assert!(!c.advance_phase(999, SyncPhase::Transfer));
}
#[test]
fn test_advance_phase_fails_on_terminal() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
c.complete_session(id);
assert!(!c.advance_phase(id, SyncPhase::Transfer));
}
#[test]
fn test_add_want_appends() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
assert!(c.add_want(id, "cid-1"));
assert!(c.add_want(id, "cid-2"));
let s = c.session(id).expect("test: session must exist");
assert_eq!(s.want_cids, vec!["cid-1", "cid-2"]);
}
#[test]
fn test_add_want_unknown() {
let mut c = coordinator();
assert!(!c.add_want(999, "cid-x"));
}
#[test]
fn test_mark_received_moves_cid() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
c.add_want(id, "cid-1");
c.add_want(id, "cid-2");
assert!(c.mark_received(id, "cid-1", 512));
let s = c.session(id).expect("test: session must exist");
assert!(!s.want_cids.contains(&"cid-1".to_owned()));
assert!(s.have_cids.contains(&"cid-1".to_owned()));
assert!(s.want_cids.contains(&"cid-2".to_owned()));
}
#[test]
fn test_mark_received_accumulates_bytes() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
c.add_want(id, "cid-a");
c.add_want(id, "cid-b");
c.mark_received(id, "cid-a", 100);
c.mark_received(id, "cid-b", 200);
assert_eq!(
c.session(id)
.expect("test: session must exist")
.transferred_bytes,
300
);
}
#[test]
fn test_mark_received_unknown() {
let mut c = coordinator();
assert!(!c.mark_received(999, "cid-x", 0));
}
#[test]
fn test_fail_session_sets_reason() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Push, 0);
assert!(c.fail_session(id, "timeout".to_owned()));
match &c
.session(id)
.expect("test: session must exist after fail_session")
.phase
{
SyncPhase::Failed { reason } => assert_eq!(reason, "timeout"),
other => panic!("expected Failed, got {other:?}"),
}
}
#[test]
fn test_fail_session_unknown() {
let mut c = coordinator();
assert!(!c.fail_session(999, "oops".to_owned()));
}
#[test]
fn test_complete_session() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Bidirectional, 0);
assert!(c.complete_session(id));
assert_eq!(
c.session(id)
.expect("test: session must exist after complete_session")
.phase,
SyncPhase::Complete
);
}
#[test]
fn test_is_terminal_complete() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
c.complete_session(id);
assert!(c
.session(id)
.expect("test: session must exist after complete_session")
.is_terminal());
}
#[test]
fn test_is_terminal_failed() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
c.fail_session(id, "error".to_owned());
assert!(c
.session(id)
.expect("test: session must exist after fail_session")
.is_terminal());
}
#[test]
fn test_is_terminal_false_for_transfer() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
c.advance_phase(id, SyncPhase::Transfer);
assert!(!c
.session(id)
.expect("test: session must exist after advance_phase")
.is_terminal());
}
#[test]
fn test_active_sessions_excludes_terminal() {
let mut c = coordinator();
let id0 = c.start_session("p0", SyncDirection::Pull, 0);
let id1 = c.start_session("p1", SyncDirection::Push, 1);
c.complete_session(id0);
let active = c.active_sessions();
let ids: Vec<u64> = active.iter().map(|s| s.session_id).collect();
assert!(!ids.contains(&id0));
assert!(ids.contains(&id1));
}
#[test]
fn test_active_sessions_sorted() {
let mut c = coordinator();
let id2 = c.start_session("p2", SyncDirection::Pull, 0);
let id1 = c.start_session("p1", SyncDirection::Pull, 1);
let id0 = c.start_session("p0", SyncDirection::Pull, 2);
let active = c.active_sessions();
let ids: Vec<u64> = active.iter().map(|s| s.session_id).collect();
assert_eq!(ids, vec![id2, id1, id0]);
}
#[test]
fn test_stats_total_sessions() {
let mut c = coordinator();
c.start_session("p0", SyncDirection::Pull, 0);
c.start_session("p1", SyncDirection::Push, 1);
assert_eq!(c.stats().total_sessions, 2);
}
#[test]
fn test_stats_completed_and_failed() {
let mut c = coordinator();
let id0 = c.start_session("p0", SyncDirection::Pull, 0);
let id1 = c.start_session("p1", SyncDirection::Push, 1);
let _id2 = c.start_session("p2", SyncDirection::Bidirectional, 2);
c.complete_session(id0);
c.fail_session(id1, "err".to_owned());
let stats = c.stats();
assert_eq!(stats.completed_sessions, 1);
assert_eq!(stats.failed_sessions, 1);
assert_eq!(stats.active_sessions, 1);
}
#[test]
fn test_stats_total_transferred_bytes() {
let mut c = coordinator();
let id0 = c.start_session("p0", SyncDirection::Pull, 0);
let id1 = c.start_session("p1", SyncDirection::Pull, 1);
c.add_want(id0, "cid-a");
c.add_want(id1, "cid-b");
c.mark_received(id0, "cid-a", 1000);
c.mark_received(id1, "cid-b", 2500);
assert_eq!(c.stats().total_transferred_bytes, 3500);
}
#[test]
fn test_pending_count() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
assert_eq!(
c.session(id)
.expect("test: session must exist")
.pending_count(),
0
);
c.add_want(id, "cid-1");
c.add_want(id, "cid-2");
assert_eq!(
c.session(id)
.expect("test: session must exist")
.pending_count(),
2
);
c.mark_received(id, "cid-1", 0);
assert_eq!(
c.session(id)
.expect("test: session must exist after mark_received")
.pending_count(),
1
);
}
#[test]
fn test_add_want_terminal_returns_false() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Push, 0);
c.complete_session(id);
assert!(!c.add_want(id, "cid-x"));
}
#[test]
fn test_advance_phase_on_failed_terminal() {
let mut c = coordinator();
let id = c.start_session("p", SyncDirection::Pull, 0);
c.fail_session(id, "err".to_owned());
assert!(!c.advance_phase(id, SyncPhase::Complete));
assert!(matches!(
c.session(id).expect("test: session must exist").phase,
SyncPhase::Failed { .. }
));
}
}