mz_cluster_controller/strategy.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// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! The pure strategy interface and the strategy implementations.
11//!
12//! A strategy is two pure functions over `(observed cluster state, now)`:
13//!
14//! - [`Strategy::update_state`] returns the durable writes the strategy wants
15//! (cut-overs, record writes/clears). The controller transacts these in the
16//! tick's first phase.
17//! - [`Strategy::desired_replicas`] returns the replica slots the strategy
18//! contributes to the cluster's desired set. The controller unions every
19//! strategy's contribution in the tick's second phase.
20//!
21//! Both are pure: same inputs, same output, no I/O. The controller is the sole
22//! mutator. Strategies never touch the [`ClusterControllerCtx`]; the controller
23//! assembles their inputs by pulling through it.
24//!
25//! [`ClusterControllerCtx`]: crate::ctx::ClusterControllerCtx
26
27use mz_repr::Timestamp;
28
29use crate::ctx::{ClusterState, ReplicaShape, StateWrite};
30
31/// A replica slot a strategy desires this tick. The reconcile kernel unions
32/// slots across strategies and matches them by [`ReplicaShape`] against the
33/// actual replica set.
34#[derive(Clone, Debug)]
35pub struct DesiredReplica {
36 pub shape: ReplicaShape,
37}
38
39/// One cluster-autoscaling strategy: a pair of pure functions the controller
40/// runs each tick. See the module docs.
41///
42/// `Send + Sync` so the controller (which holds a set of boxed strategies) can
43/// run on its own task.
44pub trait Strategy: Send + Sync {
45 /// A stable identifier used in audit attribution (which strategies desired a
46 /// create; drops carry no attribution).
47 fn name(&self) -> &'static str;
48
49 /// The durable writes this strategy wants for `state` at time `now`. The
50 /// default is no write, which suits a strategy that only ever contributes
51 /// replicas (like the baseline).
52 fn update_state(&self, _state: &ClusterState, _now: Timestamp) -> StateWrite {
53 StateWrite::default()
54 }
55
56 /// The replica slots this strategy contributes to `state`'s desired set at
57 /// time `now`.
58 fn desired_replicas(&self, state: &ClusterState, now: Timestamp) -> Vec<DesiredReplica>;
59}
60
61/// The implicit baseline strategy, always present.
62///
63/// Desires `replication_factor` replicas at the cluster's realized shape
64/// (`cluster.size` plus its AZ pool and logging). It holds the steady-state set
65/// so that the policy strategies can be purely additive. They only ever add to
66/// the baseline. With only the baseline engaged, the desired set equals the
67/// realized set, so a steady-state managed cluster reconciles to no decisions.
68#[derive(Clone, Copy, Debug, Default)]
69pub struct BaselineStrategy;
70
71/// The audit-attribution name of the baseline strategy.
72pub const BASELINE_STRATEGY_NAME: &str = "baseline";
73
74impl Strategy for BaselineStrategy {
75 fn name(&self) -> &'static str {
76 BASELINE_STRATEGY_NAME
77 }
78
79 fn desired_replicas(&self, state: &ClusterState, _now: Timestamp) -> Vec<DesiredReplica> {
80 let shape = state.realized_shape();
81 (0..state.replication_factor)
82 .map(|_| DesiredReplica {
83 shape: shape.clone(),
84 })
85 .collect()
86 }
87}