pub(crate) struct PixFillControl {
session_id: SessionId,
controller: RateController,
notify_period: Option<Duration>,
min_surb_reserve: u64,
estimator: AtomicSurbFlowEstimator,
state: Mutex<PixFillState>,
}Expand description
The Exit’s control over the single Exit → Entry keep-alive stream a Session runs.
One stream, two producers with different jobs, and this is what reconciles them:
- the SURB-level notification, sent every
surb_balance_notify_period, which tells the Entry’s balancer how many SURBs the Exit believes it holds. It is the signal that asks for more, so it must go out even when there are none left; - PIX fill, whose rate the supervisor plans once a second so that a funded cycle completes inside its own deadline even if the application never sends a byte.
The effective rate is the faster of the two, because both are the same packet: a keep-alive carrying the SURB level is exactly what fill needs to send, and sending two streams of them would double the traffic to say the same thing twice. Every shipped Entry already parses it, counts it as a consumed SURB so its balancer refills, and — since the fix that precedes this — credits it as service against the successor gate.
Fields§
§session_id: SessionId§controller: RateControllerThe rate limiter of the keep-alive stream this steers.
notify_period: Option<Duration>The configured SURB-level notification period, if the node has one.
None disables the notification half entirely; the stream then runs at the fill rate alone,
which is how a PIX Session on a node with surb_balance_notify_period: None still fills.
min_surb_reserve: u64SURBs that must remain estimated-available before a fill packet is let through.
The effective reserve for this Session, not the configured one: the Entry decides how deep
the Exit’s SURB buffer for the Session is, so a reserve above what the Entry ever intends to
supply would refuse every fill packet forever. See effective_surb_reserve.
estimator: AtomicSurbFlowEstimatorThe Session’s SURB flow estimate, which is what the reserve is measured against.
state: Mutex<PixFillState>Implementations§
Source§impl PixFillControl
impl PixFillControl
fn new( session_id: SessionId, controller: RateController, notify_period: Option<Duration>, min_surb_reserve: u64, estimator: AtomicSurbFlowEstimator, ) -> Self
Sourcepub(crate) fn start_notify(&self)
pub(crate) fn start_notify(&self)
Turns the SURB-level notification on, after its initial delay.
A no-op once the Session is draining, for the reason enter_drain
gives — and it has to be checked here rather than only there, because the one-shot delay task
holds a clone of this control and nothing bounds how long an operator may make its period.
Sourcepub(crate) fn enter_drain(&self) -> bool
pub(crate) fn enter_drain(&self) -> bool
Puts the stream into post-close drain mode, silencing the SURB-level notification.
Returns false if the stream was draining already, which is how two closes racing through
begin_surb_drain settle which of them owns the drain. The read and the write have to be one
lock acquisition for that: callers that each saw is_draining before
either wrote it would both start a drain, and both tell the supervisor about it.
The notification is the single packet admit_at exempts from the SURB
reserve, and the exemption exists for one reason: it is the message that asks the Entry for
more SURBs. It asks on behalf of a Session that a drain has already lost — the Entry itself
may well still be there, but nothing is left to act on the request — so leaving it on would
originate below the reserve into a buffer nothing is refilling, which is the one failure mode
PixFillConfig::min_surb_reserve
exists to prevent, since a return packet with no SURB holds up every packet this node
originates. With it off, notification_due is always false and the reserve is the only gate.
Sourcepub(crate) fn is_draining(&self) -> bool
pub(crate) fn is_draining(&self) -> bool
Whether this Session is draining its SURB buffer after having been closed.
Sourcepub(crate) fn drainable_surbs(&self) -> u64
pub(crate) fn drainable_surbs(&self) -> u64
The SURBs this Session could still spend on a drain, net of the reserve it must leave behind.
An estimate and an upper bound at that — ring-buffer overwrites, store eviction and invalidated relayers are all unobserved — which is why the supervisor weighs it against a cycle’s whole remaining emission rather than a discounted one. The reserve subtracted is the effective one already derived for this Session, not the configured ceiling.
fn effective(state: &PixFillState, notify_period: Option<Duration>) -> FillRate
fn apply(&self, state: &PixFillState)
Sourcepub(crate) fn admit(&self) -> bool
pub(crate) fn admit(&self) -> bool
Decides whether the next keep-alive may go out, and books it if so.
The SURB-level notification always may: it is the message that asks the Entry for more SURBs, so refusing it when SURBs are scarce is refusing to ask for the thing that is scarce.
Fill above that rate is refused while the estimate is below the reserve, and that refusal is
not an optimisation. A return packet that finds no SURB is not dropped — the routing resolver
holds it, and with it every packet this node originates, for the whole of
surb_resolution_wait. An Exit filling a cycle down to its last SURB would therefore stall
the Session it is filling for, and every other Session on the node with it.
§Which packet is the notification
Nothing about a packet says which producer it belongs to, so the notification is the first one
released no sooner than a period after the last — less a slack of
min(MAX_WAIT_CHUNK, period / 2), which is what keeps the classification from resting on
scheduling latency. The rate limiter that releases these packets sleeps in chunks of
MAX_WAIT_CHUNK, re-reads its controller on each wake, and rounds a rate into an integer
packet count, so a stream asked for one packet per period releases them a shade over or a
shade under it. Compared against the period exactly, a period the controller rounds short by a
microsecond would classify every other notification as fill — counted in
hopr_session_pix_fill_packets_total and, below the reserve, dropped. That is the signal
asking the Entry for the SURBs whose absence caused the drop, and it runs on every Exit
Session with a surb_balance_notify_period configured, PIX or not.
The slack cannot let fill masquerade as the notification: last_notify_at advances to the
instant the packet actually went out, so only the first packet past the slack window is the
notification, and every fill packet that follows it inside the period is still fill and is
still bounded by the reserve. The cap at half the period is what stops a very short period
from having its window widened away to nothing.
Sourcefn admit_at(&self, now: Instant) -> bool
fn admit_at(&self, now: Instant) -> bool
admit against a caller-supplied instant, so the classification can be tested
as the rule it is rather than by sleeping out a period and hoping the scheduler cooperates.
Sourcefn set_withholding(state: &mut PixFillState, withholding: bool)
fn set_withholding(state: &mut PixFillState, withholding: bool)
Moves the surb_starved gauge on the edges only, so it counts stalled Sessions rather than
refused packets — the same distinction PixGateBlockEpisode draws for the egress gate.
Trait Implementations§
Source§impl Drop for PixFillControl
Returns this Session’s surb_starved count if it was withholding when it went away.
impl Drop for PixFillControl
Returns this Session’s surb_starved count if it was withholding when it went away.
Drop rather than a call on the close paths, for the reason PixGateBlockEpisode gives: a
Session can end by teardown, by drain, or by its last Arc going out of scope in a task nobody
is waiting on, and this is the one mechanism that covers all three. Without it a Session that
dies mid-stall leaves the gauge standing for the life of the process.
Auto Trait Implementations§
impl !Freeze for PixFillControl
impl !RefUnwindSafe for PixFillControl
impl Send for PixFillControl
impl Sync for PixFillControl
impl Unpin for PixFillControl
impl UnsafeUnpin for PixFillControl
impl UnwindSafe for PixFillControl
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
§impl<T> Conv for T
impl<T> Conv for T
§impl<T> FmtForward for T
impl<T> FmtForward for T
§fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
self to use its Binary implementation when Debug-formatted.§fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
self to use its Display implementation when
Debug-formatted.§fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
self to use its LowerExp implementation when
Debug-formatted.§fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
self to use its LowerHex implementation when
Debug-formatted.§fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
self to use its Octal implementation when Debug-formatted.§fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
self to use its Pointer implementation when
Debug-formatted.§fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
self to use its UpperExp implementation when
Debug-formatted.§fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
self to use its UpperHex implementation when
Debug-formatted.§fn fmt_list(self) -> FmtList<Self>where
&'a Self: for<'a> IntoIterator,
fn fmt_list(self) -> FmtList<Self>where
&'a Self: for<'a> IntoIterator,
§impl<T> FutureExt for T
impl<T> FutureExt for T
§fn with_context(self, otel_cx: Context) -> WithContext<Self>
fn with_context(self, otel_cx: Context) -> WithContext<Self>
§fn with_current_context(self) -> WithContext<Self>
fn with_current_context(self) -> WithContext<Self>
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more§impl<T> Pipe for Twhere
T: ?Sized,
impl<T> Pipe for Twhere
T: ?Sized,
§fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
§fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read more§fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read more§fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
§fn pipe_borrow_mut<'a, B, R>(
&'a mut self,
func: impl FnOnce(&'a mut B) -> R,
) -> R
fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
§fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
self, then passes self.as_ref() into the pipe function.§fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
self, then passes self.as_mut() into the pipe
function.§fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
self, then passes self.deref() into the pipe function.§impl<T> Pointable for T
impl<T> Pointable for T
§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
§impl<T> Tap for T
impl<T> Tap for T
§fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
Borrow<B> of a value. Read more§fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
BorrowMut<B> of a value. Read more§fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
AsRef<R> view of a value. Read more§fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
AsMut<R> view of a value. Read more§fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
Deref::Target of a value. Read more§fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
Deref::Target of a value. Read more§fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
.tap() only in debug builds, and is erased in release builds.§fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
.tap_mut() only in debug builds, and is erased in release
builds.§fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
.tap_borrow() only in debug builds, and is erased in release
builds.§fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
.tap_borrow_mut() only in debug builds, and is erased in release
builds.§fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
.tap_ref() only in debug builds, and is erased in release
builds.§fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
.tap_ref_mut() only in debug builds, and is erased in release
builds.§fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
.tap_deref() only in debug builds, and is erased in release
builds.