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: DurationHow 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: DurationTime 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: usizeRecent 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: usizeMinimum 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:
| budget | optimum | surface |
|---|---|---|
| 32 | 12 slots | 8 slots +29%, 16 +13%, 24 +115% |
| 64 | 22 slots | 16 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: usizeSamples 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: usizeWindows 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: OpeningPolicyWhether a disagreement between the opening and the first stable model answer should be confirmed by a bounded startup bracket.
regression_tolerance: f64Relative 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: usizeHow 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: DurationWall-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: DurationMaximum 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: u64Absolute queue drift tolerated in one cost window.
max_buffer_drift_fraction: f64Queue drift tolerated as a fraction of producer progress in the window.
cap_history: DurationWall-clock history retained for producer scalability caps.
cap_persistence: DurationContinuous corroborating evidence required before slack or source saturation is allowed to constrain a move.
trace_capacity: usizeMaximum number of requested/actual samples retained in a report.
min_consumer_threads: usizeThe consumer never drops below this.
min_producer_slots: usizeProducer 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.