pub struct MultiController { /* private fields */ }Expand description
Several sources, one clock loop.
This is the structural fix for multi-source selection, and it is worth
stating why the obvious alternative does not work. The daemon used to give
every source its own SyncController — its own register and its own
frequency, drain and budget — and let only the selected one reach the clock.
That leaves every unselected source having produced a plan that never
happened, and seven different ways of cleaning up after that plan were
measured on the three-server rig. Every one was worse than leaving the
wrong books in place, which is the signature of a wrong model rather than a
wrong patch.
The model was wrong. A frequency correction, a drain and its budget are properties of THE CLOCK, of which there is one; only the sample history is a property of a source. So the registers are per-source and everything else is shared, and an unselected source never produces a plan in the first place — there is nothing to revert, confirm or adopt, because nothing was ever booked. The entire class of bug is gone rather than patched.
With one source this is arithmetically identical to what shipped before it, which is checked by running the corpus against the previous binary.
Implementations§
Source§impl MultiController
impl MultiController
pub fn new(config: DisciplineConfig, sources: usize) -> Self
Sourcepub fn revert_last_plan(&mut self) -> bool
pub fn revert_last_plan(&mut self) -> bool
Undo the bookkeeping of the last plan, because the driver refused it.
The loop’s arithmetic has to describe what the clock actually did.
A plan books its own effects the moment it is produced: the frequency
change tilts every stored sample, a step shifts them, and the drain
budget starts counting down. The caller then hands the command to the
platform — which can refuse it. clock_adjtime returns EPERM the
moment CAP_SYS_TIME goes away, and a seccomp policy or a container
with a read-only clock refuses it too.
Without this, a refusal is silent and cumulative. The register carries corrections that never happened, the regression reads that history as truth, and the daemon reports itself synchronised while the clock free runs — the worst failure a time daemon has, because nothing looks wrong.
Returns whether there was a plan to revert.
Sourcepub fn confirm_last_plan(&mut self)
pub fn confirm_last_plan(&mut self)
Confirm the last plan reached the clock, so it can no longer be undone.
pub fn freq_ppm(&self) -> f64
pub fn drain_ppm(&self) -> f64
pub fn applied_ppm(&self) -> f64
pub fn poll_log2(&self) -> i8
Sourcepub fn poll_interval_s(&self) -> f64
pub fn poll_interval_s(&self) -> f64
The loop’s current poll interval, in seconds.
A source that is not steering still has to be scheduled, and the poll interval belongs to the loop rather than to any one source.
pub fn samples(&self) -> usize
pub fn samples_from(&self, index: usize) -> usize
pub fn sources(&self) -> usize
Sourcepub fn preload_frequency(&mut self, freq_ppm: f64)
pub fn preload_frequency(&mut self, freq_ppm: f64)
Seed the frequency estimate from persisted drift, so a restart does not re-learn what was already known.
Sourcepub fn retry_interval_s(&self) -> f64
pub fn retry_interval_s(&self) -> f64
The interval to use when an exchange is lost — no plan is produced, but the caller still needs to know when to try again.
Sourcepub fn drain_completes_at(&self) -> Option<f64>
pub fn drain_completes_at(&self) -> Option<f64>
When the running drain will have spent its budget, if one is running.
Sourcepub fn poll_drain(&mut self, mono_now_s: f64) -> Option<ClockCommand>
pub fn poll_drain(&mut self, mono_now_s: f64) -> Option<ClockCommand>
End the drain if its budget is spent, returning the command that leaves the clock running at the frequency term alone.
Callers must invoke this as they advance time — the daemon by waking for it, the simulator at each substep — or the drain runs on past its budget and overshoots, which is the behaviour this exists to end.
Sourcepub fn observe(
&mut self,
index: usize,
mono_now_s: f64,
sample: Sample,
) -> Estimate
pub fn observe( &mut self, index: usize, mono_now_s: f64, sample: Sample, ) -> Estimate
Record one exchange from one source. Measurement only — this never touches the clock, so calling it for a source that is not selected costs nothing and leaves nothing to undo.
Sourcepub fn estimate(&mut self, index: usize, mono_now_s: f64) -> Estimate
pub fn estimate(&mut self, index: usize, mono_now_s: f64) -> Estimate
This source’s current view of the clock.
Regression once it has enough spread; before that the lowest-delay single sample, which is the least contaminated reading available.
Sourcepub fn steer(
&mut self,
est: Estimate,
mono_now_s: f64,
leap_pending: bool,
) -> ControllerStep
pub fn steer( &mut self, est: Estimate, mono_now_s: f64, leap_pending: bool, ) -> ControllerStep
Steer the clock from one source’s estimate.
Call this for the SELECTED source only, passing the Estimate that
MultiController::observe just returned. Its effects are booked
against every register, because the correction lands on the one clock
they all measure.
Taking the estimate rather than re-deriving it is not tidiness: the
regression is the expensive part of a step, and computing it in
observe and again here doubled the cost of the whole discipline —
measured 13,614 to 26,169 Ir per step.