///|
/// Classification of a physical clock reading relative to the prior sample.
pub(all) enum ClockReadingStatus {
ClockReadingAccepted
ClockRollback(Int)
ClockForwardJump(Int)
} derive(Eq, Debug)
///| Thresholds for diagnosing host-clock behavior. HLC remains monotonic even
///|
/// when a reading is suspicious; this policy is observability, not rejection.
pub(all) struct ClockDiagnosticPolicy {
maximum_rollback : Int
maximum_forward_jump : Int
} derive(Eq, Debug)
///|
/// One classified host-clock sample.
pub(all) struct ClockSample {
reading : Int
previous : Int
status : ClockReadingStatus
} derive(Eq, Debug)
///|
/// Immutable diagnostics accumulated for one clock source.
pub struct ClockDiagnostics {
policy : ClockDiagnosticPolicy
last_reading : Int
samples : Array[ClockSample]
rollback_count : Int
forward_jump_count : Int
} derive(Debug)
///|
/// Policy and sample validation failures.
pub(all) enum ClockDiagnosticError {
NegativeRollbackThreshold(Int)
NegativeForwardJumpThreshold(Int)
NegativeClockReading(Int)
} derive(Eq, Debug)
///|
/// Construct diagnostic thresholds.
pub fn ClockDiagnosticPolicy::new(
maximum_rollback : Int,
maximum_forward_jump : Int,
) -> Result[ClockDiagnosticPolicy, ClockDiagnosticError] {
if maximum_rollback < 0 {
Err(NegativeRollbackThreshold(maximum_rollback))
} else if maximum_forward_jump < 0 {
Err(NegativeForwardJumpThreshold(maximum_forward_jump))
} else {
Ok({ maximum_rollback, maximum_forward_jump })
}
}
///|
/// Start diagnostics at a known physical reading.
pub fn ClockDiagnostics::new(
policy : ClockDiagnosticPolicy,
initial_reading : Int,
) -> Result[ClockDiagnostics, ClockDiagnosticError] {
if initial_reading < 0 {
Err(NegativeClockReading(initial_reading))
} else {
Ok({
policy,
last_reading: initial_reading,
samples: [],
rollback_count: 0,
forward_jump_count: 0,
})
}
}
///|
/// Classify and append one reading.
pub fn ClockDiagnostics::observe(
self : ClockDiagnostics,
reading : Int,
) -> Result[ClockDiagnostics, ClockDiagnosticError] {
if reading < 0 {
return Err(NegativeClockReading(reading))
}
let difference = reading - self.last_reading
let status = if difference < 0 &&
0 - difference > self.policy.maximum_rollback {
ClockRollback(0 - difference)
} else if difference > self.policy.maximum_forward_jump {
ClockForwardJump(difference)
} else {
ClockReadingAccepted
}
let samples : Array[ClockSample] = []
for sample in self.samples {
samples.push(sample)
}
samples.push({ reading, previous: self.last_reading, status })
let rollback_increment = match status {
ClockRollback(_) => 1
_ => 0
}
let forward_increment = match status {
ClockForwardJump(_) => 1
_ => 0
}
Ok({
policy: self.policy,
last_reading: reading,
samples,
rollback_count: self.rollback_count + rollback_increment,
forward_jump_count: self.forward_jump_count + forward_increment,
})
}
///|
/// Recorded samples in observation order.
pub fn ClockDiagnostics::samples(self : ClockDiagnostics) -> Array[ClockSample] {
let output : Array[ClockSample] = []
for sample in self.samples {
output.push(sample)
}
output
}
///|
/// Most recent physical reading.
pub fn ClockDiagnostics::last_reading(self : ClockDiagnostics) -> Int {
self.last_reading
}
///|
/// Number of excessive rollbacks.
pub fn ClockDiagnostics::rollback_count(self : ClockDiagnostics) -> Int {
self.rollback_count
}
///|
/// Number of excessive forward jumps.
pub fn ClockDiagnostics::forward_jump_count(self : ClockDiagnostics) -> Int {
self.forward_jump_count
}
///|
/// Whether any suspicious sample has been observed.
pub fn ClockDiagnostics::has_anomaly(self : ClockDiagnostics) -> Bool {
self.rollback_count > 0 || self.forward_jump_count > 0
}
///|
/// Only anomalous samples, suitable for telemetry emission.
pub fn ClockDiagnostics::anomalies(
self : ClockDiagnostics,
) -> Array[ClockSample] {
let output : Array[ClockSample] = []
for sample in self.samples {
match sample.status {
ClockReadingAccepted => ()
ClockRollback(_) | ClockForwardJump(_) => output.push(sample)
}
}
output
}
///| Clear accumulated samples and counters while retaining policy and the last
///|
/// reading as the next comparison baseline.
pub fn ClockDiagnostics::reset(self : ClockDiagnostics) -> ClockDiagnostics {
{
policy: self.policy,
last_reading: self.last_reading,
samples: [],
rollback_count: 0,
forward_jump_count: 0,
}
}