pub(crate) struct ProxyRecorder {
legs: [[DirectionCounters; 2]; 2],
classes: OnceLock<Vec<ClassCounters>>,
default_class: ClassCounters,
unshapeable: ClassCounters,
}Expand description
Proxy-scoped shaping-counter storage — the storage behind ProxyStats.
One per TransparentProxy, owned by
its control plane and handed to each session’s ShapeRecorder as that
session is attached, so it lives for as long as the proxy does and no
session’s ending takes anything out of it.
Every counter here is charged by the same call that charges the session
recorder — ShapeRecorder’s note_* methods forward — which is what
makes the two recorders unable to disagree. The data path pays one extra
relaxed fetch_add per figure and one Option test, on a path that has
already done a write_all.
Fields§
§legs: [[DirectionCounters; 2]; 2]The four cells of ProxyStats::per_leg: legs[leg][direction],
indexed by leg_index then Direction::index.
classes: OnceLock<Vec<ClassCounters>>The class rows, installed by the first session to attach with a class
to install — see Self::adopt_classes for the two kinds of session
that have none and must not win this.
A OnceLock and not a Mutex, because the list must be fixed:
a Class::Rule(index) is an index into the class list of the
scheduler that produced it, so rows that could be resized under a
running session would silently relabel every figure in them. First
writer wins; a later session whose class list differs charges
Self::default_class, which Self::row does and
ProxyStats::classes states.
Empty until the first shaped session, so a proxy that has accepted nothing, or only unshaped sessions, reports no class rows rather than rows invented from a profile nothing ran under.
default_class: ClassCounters§unshapeable: ClassCountersImplementations§
Source§impl ProxyRecorder
impl ProxyRecorder
Sourcefn adopt_classes(&self, names: &[String]) -> bool
fn adopt_classes(&self, names: &[String]) -> bool
Size the class rows from names if nothing has yet, and answer
whether the installed rows are names — which is whether a session
running that class list may charge them by index.
The comparison is over names and order, the same pair
same_classes compares in session.rs and for the same reason: that
pair is what makes a Class::Rule(index) mean the same thing to the
scheduler that produced it and to the row it is charged to. The same
names in a different order would charge every class to another one’s
row without a single count going missing.
§An empty list never sizes anything
A list with no rows in it names no row, so letting it win the
OnceLock would install an empty row set that nothing can ever match
again: every classed session accepted for the rest of the proxy’s life
would find rows it did not match and charge Self::default_class,
with ProxyStats::classes empty forever. Every figure right, every
label gone — which is precisely the relabelling this whole sizing rule
exists to prevent, and it would arrive silently and be unrecoverable
without restarting the proxy.
The way an empty list used to get here was a ShapeProfile with no
classes, which the constructor accepted because it validated each
class it was given and had nothing to say about being given none.
ShapeProfile::try_new now refuses that outright as
ShapeError::NoClasses, so no
profile can carry an empty list to this call and the branch below is
no longer reachable from any public path.
It stays because of what it costs against what it prevents: two lines
and a comparison that is already being made, against a proxy-wide,
silent, restart-only failure. Answering false costs a caller nothing
in any case — a session with no Class::Rule to charge is unaffected
by the flag, and Class::Default and Class::Unshapeable are
unaffected by it always.
Sourcefn classes(&self) -> &[ClassCounters]
fn classes(&self) -> &[ClassCounters]
The class rows, or an empty slice before any shaped session attached.
Sourcefn arrival(&self, side: ProxySide) -> &DirectionCounters
fn arrival(&self, side: ProxySide) -> &DirectionCounters
The cell a unit read on side is charged to.
Sourcefn departure(&self, side: ProxySide) -> &DirectionCounters
fn departure(&self, side: ProxySide) -> &DirectionCounters
The cell a unit read on side is charged to when it is written:
the other leg, the same direction.
Sourcefn row(&self, class: Class, sized: bool) -> &ClassCounters
fn row(&self, class: Class, sized: bool) -> &ClassCounters
The row one class’s units are charged to.
sized is whether the charging session’s class list is the one these
rows were sized from. When it is not, a Class::Rule index names a
row belonging to a different rule, so the unit goes to the default
row instead — the same answer ShapeRecorder::row gives an
impossible index, and for the stronger of the two reasons: here the
index is not impossible, it is plausible and wrong.
Class::Unshapeable is unaffected either way. That row is not
named for a rule, so no reconfiguration can make it mean something
else.
Sourcepub(crate) fn snapshot(&self) -> ProxyStats
pub(crate) fn snapshot(&self) -> ProxyStats
Read every counter.
Allocates: one Vec and one String per class row. A reader’s call —
the control plane’s — never the data path’s.
ProxyStats::sessions is computed here, from the two arrival
cells and the class rows, which is why no writer maintains it.
Sourcepub(crate) fn reset(&self)
pub(crate) fn reset(&self)
Zero every counter this proxy holds, keeping the class rows and their names.
The rows survive because they are what a Class::Rule(index) means:
dropping them would let the next session install a different class
list and charge figures under it, which is precisely the relabelling
Self::adopt_classes exists to prevent. A reset moves the counters
to zero, not the schema.