Skip to main content

BrokerConfig

Struct BrokerConfig 

Source
pub struct BrokerConfig {
Show 19 fields pub sample_interval: Duration, pub steady_probe_interval: Option<Duration>, pub warmup: Duration, pub smoothing_windows: usize, pub deadband_threads: usize, pub blackout_samples: usize, pub ratify_samples: usize, pub opening_policy: OpeningPolicy, pub regression_tolerance: f64, pub resurvey_distance: usize, pub resurvey_persistence: Duration, pub resize_timeout: Duration, pub max_buffer_drift_items: u64, pub max_buffer_drift_fraction: f64, pub cap_history: Duration, pub cap_persistence: Duration, pub trace_capacity: usize, pub min_consumer_threads: usize, pub min_producer_slots: usize,
}
Expand description

Tuning, all of it overridable.

Every default below was arrived at by fixing an observed failure, so they carry real information — but none is a law, and a caller with different timings should move them.

Fields§

§sample_interval: Duration

How often to sample and decide.

100 ms by default, with a 400 ms warm-up over it. An earlier pairing of 250 ms and 150 ms was self-inconsistent: it failed this type’s own warmup >= 2 * sample_interval rule, so every broker built from the defaults failed to build. Nothing caught it, because every unit test passed an explicit config — the defaults were the one combination never exercised. There is now a test that builds from them.

250 ms was chosen to match rapidgzip’s worker-retirement hysteresis, on the grounds that instantaneous fields sampled at that period alias against their own duty cycle. That reasoning applies to a producer that reports instantaneous worker counts; the signals used here are an integrated busy counter and a demand estimate, neither of which aliases.

§steady_probe_interval: Option<Duration>

How often a responsive broker probes after convergence.

None (the default) uses Self::sample_interval, preserving the fully responsive behavior. A longer interval changes only settled monitoring; warm-up, calibration, resizing, blackout, and ratification continue at sample_interval.

§warmup: Duration

Time discarded before the first decision.

Load-bearing, not merely tuned. Every workload measured reported ~99% consumer starvation in its first 75 ms, because the consumer’s threads are still starting and nothing has been processed yet. With no warm-up, every input looks producer-bound and the broker’s first move is always wrong.

Must span at least two sampling intervals, or the first decision rests on a single window.

§smoothing_windows: usize

Recent windows averaged into the rate estimates.

Deciding on a single window makes the broker a noise amplifier: a real consumer throws the occasional idle sample from a batch boundary or a page fault. Averaging is what makes the signal steady, rather than trying to out-tune the noise downstream of it.

One disables smoothing.

§deadband_threads: usize

Minimum distance, in threads, between the model’s answer and the current split before the broker will act.

Moves are not free — each one rebuilds per-thread state on both sides, including a multi-MB record buffer per new consumer worker — so the broker should not spend one on a difference it cannot be sure about.

§This value is not yet derived, and should be

The measurements it was originally justified from are void. They were taken through a path that did not enforce the thread budget, which made a sharply peaked throughput surface look like a wide plateau, and the conclusion drawn — “precision near the answer is worth almost nothing” — does not survive re-measurement.

What the corrected sweeps show, on a decode-bound single-cell workload:

budgetoptimumsurface
3212 slots8 slots +29%, 16 +13%, 24 +115%
6422 slots16 slots +17%, 28 +5%, 40 +34%

Two threads is defensible against both — the broker landed 5.0% and 3.4% off the best swept point — but that is an observation, not a derivation, and the two budgets differ enough in tolerance that a single constant is unlikely to be right for both. It matters most at small budgets, where two threads is a large fraction of the answer.

§blackout_samples: usize

Samples discarded after a move before the model is trusted again.

Two distinct effects need to decay. The pools converge toward their new size over some interval, so a window spanning a move measures a split that no longer exists; and the move itself does work — a new consumer worker builds its thread state and allocates its record set — which inflates busy time without moving any items, biasing the very ratio the decision was made from.

Must be at least smoothing_windows, or the smoothed estimate still contains pre-move windows when the broker next decides.

§ratify_samples: usize

Windows of throughput evidence gathered before a move is ratified. The default ten-window block spans one second at the default 100 ms cadence. Applications should validate its power against stationary windows from their real workload; a deliberately changing or pausing workload is not a statistically identifiable ratification population.

§opening_policy: OpeningPolicy

Whether a disagreement between the opening and the first stable model answer should be confirmed by a bounded startup bracket.

§regression_tolerance: f64

Relative throughput loss that counts as a regression rather than noise.

Only a regression reverts a move. Absence of improvement does not, because the surface is flat near the optimum: “no better” is the expected reading for a correct move, and treating it as failure would revert the broker to wherever it happened to start. Flink’s equivalent check, detectIneffectiveScaleUp, compares against a model-predicted increase for this reason, and ships disabled by default.

§resurvey_distance: usize

How far the model’s answer must drift from the settled split, in threads, before the broker re-opens a decision it had closed.

Settling must not mean going deaf: a file ending or a different sample beginning genuinely changes the costs. But adapting continuously spends moves on noise forever, and each move rebuilds per-thread state on both sides. So the broker settles and then sleeps until surprised. This value is only an absolute floor: observed cost-share uncertainty can widen the effective threshold, and persistent evidence is required before the decision re-opens. Keeping the floor at one preserves sensitivity to a one-slot regime change on small budgets without making noisy large budgets unstable.

§Why drift rather than a throughput CUSUM

The obvious alternative is to watch throughput and re-arm when it falls — a one-sided CUSUM, and the form the autoscaling literature usually reaches for. It is genuinely one-sided, and that turns out to matter here: when a consumer suddenly gets cheaper, throughput does not fall at all — the pipeline simply stays pinned at the old producer-limited rate while a much better split goes unclaimed. A CUSUM sleeps through it. The solved target moves immediately, because it is computed from the cost ratio rather than from the outcome, so watching the model detects change in both directions with one mechanism instead of two.

§resurvey_persistence: Duration

Wall-clock time model drift must persist before a settled split is re-opened.

Distance alone is not enough, and the reason is that the noise in the solved target scales with the budget while a fixed distance does not. Measured on a 116-second Flex run at -t 64: with distance as the only gate the broker re-opened 18 times and never reported convergence, while the same build at -t 32 settled once in 1.4 s and never moved again — identical workload, identical noise in the share, twice the noise in threads.

Requiring duration-based persistence separates the two cleanly, because it keys on something that actually distinguishes them: sampling noise does not survive a second of consecutive windows, and a real regime change does. It also costs nothing when there is no drift.

§resize_timeout: Duration

Maximum time to wait for the shrinking side to release capacity before aborting the broker. The other side is never grown before this acknowledgement arrives.

§max_buffer_drift_items: u64

Absolute queue drift tolerated in one cost window.

§max_buffer_drift_fraction: f64

Queue drift tolerated as a fraction of producer progress in the window.

§cap_history: Duration

Wall-clock history retained for producer scalability caps.

§cap_persistence: Duration

Continuous corroborating evidence required before slack or source saturation is allowed to constrain a move.

§trace_capacity: usize

Maximum number of requested/actual samples retained in a report.

§min_consumer_threads: usize

The consumer never drops below this.

§min_producer_slots: usize

Producer concurrency never drops below this, in aggregate.

One is right for a producer whose limit is a pure runtime throttle, and that is worth checking rather than assuming. rapidgzip used to read this limit while choosing a backend, so anything below two committed a decoder to sequential decoding irreversibly — a caller on such a version, or on any producer that couples its limit to a one-time decision, needs this at two or more.

Measured on the shared-pool decoder: an aggregate limit of 1 leaves the path at DenseMembers and still reports a demand of 12, so admission is genuinely decoupled and the floor can be a single slot. Note this is aggregate, not per input — scaling it by input count would reserve half a 32-thread budget on an 8-file run that needs no decode at all.

Trait Implementations§

Source§

impl Clone for BrokerConfig

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for BrokerConfig

Source§

impl Debug for BrokerConfig

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for BrokerConfig

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more