use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::message::{AnnounceBody, PortIdentity};
const FOREIGN_MASTER_THRESHOLD: usize = 2;
const FOREIGN_MASTER_WINDOW_MULTIPLIER: u32 = 4;
#[derive(Debug, Clone)]
pub struct ForeignMasterRecord {
pub port_identity: PortIdentity,
pub announce: AnnounceBody,
pub receive_times: Vec<Instant>,
pub qualified: bool,
}
impl ForeignMasterRecord {
fn new(port_identity: PortIdentity, announce: AnnounceBody) -> Self {
Self {
port_identity,
announce,
receive_times: Vec::new(),
qualified: false,
}
}
}
#[derive(Debug)]
pub struct ForeignMasterTable {
records: HashMap<PortIdentity, ForeignMasterRecord>,
window: Duration,
threshold: usize,
max_records: usize,
}
impl ForeignMasterTable {
pub fn new(announce_interval_secs: f64, max_records: usize) -> Self {
let window = Duration::from_secs_f64(
announce_interval_secs * FOREIGN_MASTER_WINDOW_MULTIPLIER as f64,
);
Self {
records: HashMap::new(),
window,
threshold: FOREIGN_MASTER_THRESHOLD,
max_records,
}
}
pub fn record_announce(
&mut self,
source: PortIdentity,
announce: AnnounceBody,
now: Instant,
) -> bool {
let record = self
.records
.entry(source)
.or_insert_with(|| ForeignMasterRecord::new(source, announce.clone()));
record.announce = announce;
record.receive_times.push(now);
let cutoff = now.checked_sub(self.window).unwrap_or(now);
record.receive_times.retain(|&t| t >= cutoff);
record.qualified = record.receive_times.len() >= self.threshold;
let qualified = record.qualified;
if self.records.len() > self.max_records {
self.evict_oldest_unqualified(source);
}
qualified
}
fn evict_oldest_unqualified(&mut self, keep: PortIdentity) {
if self.records.len() <= self.max_records {
return;
}
let victim = self
.records
.iter()
.filter(|(id, r)| !r.qualified && **id != keep)
.min_by_key(|(_, r)| r.receive_times.first().copied())
.map(|(id, _)| *id);
if let Some(id) = victim {
self.records.remove(&id);
}
}
pub fn qualified_masters(&self) -> Vec<&ForeignMasterRecord> {
self.records.values().filter(|r| r.qualified).collect()
}
pub fn get(&self, identity: &PortIdentity) -> Option<&ForeignMasterRecord> {
self.records.get(identity)
}
pub fn remove(&mut self, identity: &PortIdentity) -> Option<ForeignMasterRecord> {
self.records.remove(identity)
}
pub fn expire_stale(&mut self, now: Instant) {
let cutoff = now.checked_sub(self.window).unwrap_or(now);
self.records.retain(|_, record| {
record.receive_times.retain(|&t| t >= cutoff);
!record.receive_times.is_empty()
});
for record in self.records.values_mut() {
record.qualified = record.receive_times.len() >= self.threshold;
}
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn clear(&mut self) {
self.records.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::ClockQuality;
use rtime_core::timestamp::PtpTimestamp;
fn make_port_identity(id: u8) -> PortIdentity {
PortIdentity {
clock_identity: [id; 8],
port_number: 1,
}
}
fn make_announce_body() -> AnnounceBody {
AnnounceBody {
origin_timestamp: PtpTimestamp::ZERO,
current_utc_offset: 37,
grandmaster_priority1: 128,
grandmaster_clock_quality: ClockQuality {
clock_class: 6,
clock_accuracy: 0x21,
offset_scaled_log_variance: 0x4E5D,
},
grandmaster_priority2: 128,
grandmaster_identity: [0; 8],
steps_removed: 0,
time_source: 0x20,
}
}
#[test]
fn first_announce_not_qualified() {
let mut table = ForeignMasterTable::new(2.0, 5);
let now = Instant::now();
let pi = make_port_identity(1);
let qualified = table.record_announce(pi, make_announce_body(), now);
assert!(!qualified);
assert_eq!(table.qualified_masters().len(), 0);
}
#[test]
fn second_announce_qualifies() {
let mut table = ForeignMasterTable::new(2.0, 5);
let now = Instant::now();
let pi = make_port_identity(1);
table.record_announce(pi, make_announce_body(), now);
let qualified =
table.record_announce(pi, make_announce_body(), now + Duration::from_secs(2));
assert!(qualified);
assert_eq!(table.qualified_masters().len(), 1);
}
#[test]
fn old_announces_expire() {
let mut table = ForeignMasterTable::new(2.0, 5);
let now = Instant::now();
let pi = make_port_identity(1);
table.record_announce(pi, make_announce_body(), now);
table.record_announce(pi, make_announce_body(), now + Duration::from_secs(1));
assert_eq!(table.qualified_masters().len(), 1);
table.expire_stale(now + Duration::from_secs(10));
assert_eq!(table.qualified_masters().len(), 0);
assert!(table.is_empty());
}
#[test]
fn multiple_foreign_masters() {
let mut table = ForeignMasterTable::new(2.0, 5);
let now = Instant::now();
for i in 1..=3 {
let pi = make_port_identity(i);
table.record_announce(pi, make_announce_body(), now);
table.record_announce(pi, make_announce_body(), now + Duration::from_secs(1));
}
assert_eq!(table.len(), 3);
assert_eq!(table.qualified_masters().len(), 3);
}
#[test]
fn get_specific_record() {
let mut table = ForeignMasterTable::new(2.0, 5);
let now = Instant::now();
let pi = make_port_identity(42);
table.record_announce(pi, make_announce_body(), now);
let record = table.get(&pi).unwrap();
assert_eq!(record.port_identity, pi);
}
#[test]
fn remove_record() {
let mut table = ForeignMasterTable::new(2.0, 5);
let now = Instant::now();
let pi = make_port_identity(1);
table.record_announce(pi, make_announce_body(), now);
assert_eq!(table.len(), 1);
table.remove(&pi);
assert!(table.is_empty());
}
#[test]
fn clear_table() {
let mut table = ForeignMasterTable::new(2.0, 5);
let now = Instant::now();
for i in 1..=3 {
table.record_announce(make_port_identity(i), make_announce_body(), now);
}
assert_eq!(table.len(), 3);
table.clear();
assert!(table.is_empty());
}
#[test]
fn max_records_eviction() {
let mut table = ForeignMasterTable::new(2.0, 2); let now = Instant::now();
table.record_announce(make_port_identity(1), make_announce_body(), now);
table.record_announce(make_port_identity(2), make_announce_body(), now);
table.record_announce(make_port_identity(3), make_announce_body(), now);
assert!(table.len() <= 3); }
}