Skip to main content

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;