mz_compute/compute_state/peek_budget.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5
6//! What a worker activation may spend walking index peeks before it hands the worker back.
7
8use mz_compute_types::dyncfgs::{
9 ENABLE_INDEX_PEEK_OFFLOAD, INDEX_PEEK_ACTIVATION_BUDGET, INDEX_PEEK_INLINE_BUDGET,
10};
11use mz_dyncfg::{ConfigSet, ConfigValHandle};
12
13/// The parameters an [`InlineBudget`] is read from, as handles: every activation reads them, and
14/// by-name lookups would put three searches of the configuration set on the worker's loop.
15struct InlineBudgetConfig {
16 enabled: ConfigValHandle<bool>,
17 per_peek: ConfigValHandle<usize>,
18 aggregate: ConfigValHandle<usize>,
19}
20
21impl InlineBudgetConfig {
22 fn new(config: &ConfigSet) -> Self {
23 Self {
24 enabled: ENABLE_INDEX_PEEK_OFFLOAD.handle(config),
25 per_peek: INDEX_PEEK_INLINE_BUDGET.handle(config),
26 aggregate: INDEX_PEEK_ACTIVATION_BUDGET.handle(config),
27 }
28 }
29
30 /// Reads the parameters in effect now into the fuel of one activation.
31 fn arm(&self) -> ActivationBudget {
32 if !self.enabled.get() {
33 return ActivationBudget::Unbounded;
34 }
35
36 // Zero must not wedge. A per-peek budget of zero suspends every scan before it walks a
37 // position, and every suspension is an offload, so point lookups would pay for a task and
38 // a permit to walk nothing. An aggregate of zero passes every peek over forever.
39 ActivationBudget::Bounded {
40 per_peek: self.per_peek.get().max(1),
41 remaining: self.aggregate.get().max(1),
42 }
43 }
44}
45
46/// The fuel an activation may spend walking index peeks on the worker, and what one peek may take
47/// of it.
48///
49/// The per-peek budget decides placement: a peek that outruns it moves off the worker. The
50/// aggregate exists because the sweep visits every pending peek, so a per-peek budget alone would
51/// let N peeks cost N times that budget in one pass.
52///
53/// Both count cursor positions, the unit the scan charges.
54enum ActivationBudget {
55 /// Every peek walks on the worker until it answers or until its rows belong in the peek stash,
56 /// and nothing is passed over.
57 ///
58 /// What the kill switch restores. A stash-bound peek still offloads, because the driver that
59 /// writes to the stash is the offloaded one, so this restores where an ordinary peek runs and
60 /// not a guarantee that none leaves the worker.
61 Unbounded,
62 /// A peek may spend `per_peek` before it is offloaded, and all peeks together may spend
63 /// `remaining` before the rest of this activation's work gets the worker back.
64 Bounded { per_peek: usize, remaining: usize },
65}
66
67/// The fuel of the activation under way, armed from the parameters by the first peek that asks for
68/// a slice of it.
69///
70/// Arming at the grant is what reads the parameters as late as anything can. Commands drain in
71/// full before the sweep that begins an activation, so a peek arriving on that path is granted a
72/// slice before any activation has begun, and a budget that only an activation's start armed would
73/// hand that peek whatever the last one left. The constructor cannot arm it either: `ConfigSet`
74/// handles read the live value, but a value read there is read before `handle_create_instance`
75/// applies the controller's snapshot, and an empty snapshot leaves the defaults in place until the
76/// first `UpdateConfiguration`. Since the offload's own flag defaults off, an eagerly armed budget
77/// is an unbounded one, and it would go to every peek in a reconnecting controller's backlog.
78pub(super) struct InlineBudget {
79 config: InlineBudgetConfig,
80 /// The fuel of the activation under way, or `None` while no peek has asked for a slice since
81 /// the activation began.
82 activation: Option<ActivationBudget>,
83}
84
85impl InlineBudget {
86 /// A budget reading `config`, whose first grant arms the first activation.
87 pub(super) fn new(config: &ConfigSet) -> Self {
88 Self {
89 config: InlineBudgetConfig::new(config),
90 activation: None,
91 }
92 }
93
94 /// Begins an activation, discarding what the previous one left. Nothing else refills the fuel.
95 pub(super) fn start_activation(&mut self) {
96 self.activation = None;
97 }
98
99 /// The fuel one peek's slice may spend, or `None` when this activation has none left to give.
100 ///
101 /// A peek gets its whole per-peek budget or nothing, so the aggregate can overrun by one
102 /// budget.
103 ///
104 /// A caller granted `None` must pass the peek over. Stepping with no fuel suspends the scan
105 /// without walking anything, offloading a peek that never had its inline turn.
106 pub(super) fn grant(&mut self) -> Option<usize> {
107 let Self { config, activation } = self;
108 match activation.get_or_insert_with(|| config.arm()) {
109 ActivationBudget::Unbounded => Some(usize::MAX),
110 ActivationBudget::Bounded {
111 per_peek,
112 remaining,
113 } => (*remaining > 0).then_some(*per_peek),
114 }
115 }
116
117 /// Charges the activation for the positions a slice walked. An unarmed budget has nothing to
118 /// charge, since a slice is only ever walked out of fuel this granted.
119 pub(super) fn charge(&mut self, spent: usize) {
120 match &mut self.activation {
121 Some(ActivationBudget::Bounded { remaining, .. }) => {
122 *remaining = remaining.saturating_sub(spent)
123 }
124 Some(ActivationBudget::Unbounded) | None => {}
125 }
126 }
127
128 /// What is left of this activation's aggregate, or `None` when the activation is unarmed or
129 /// unbounded.
130 #[cfg(test)]
131 pub(super) fn remaining(&self) -> Option<usize> {
132 match &self.activation {
133 Some(ActivationBudget::Bounded { remaining, .. }) => Some(*remaining),
134 Some(ActivationBudget::Unbounded) | None => None,
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests;