Skip to main content

mz_adapter_types/
cluster_state.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//! Plain-data mirror of a managed cluster's durable configuration.
11//!
12//! These types carry the slices of a managed cluster's config that a caller
13//! reasons over without touching the catalog or SQL layers. Keeping them free
14//! of catalog and SQL dependencies lets one component reason over the config
15//! and another project the live config onto the same types, without either
16//! depending on the other.
17//!
18//! [`ExpectedClusterState`] is a compare-and-append witness. A caller captures
19//! it from a config snapshot and pairs it with a conditional write. The applier
20//! re-projects the live config and applies the write only if it still equals
21//! the witness.
22
23use std::collections::BTreeSet;
24use std::time::Duration;
25
26use mz_compute_types::config::ComputeReplicaLogging;
27use mz_repr::Timestamp;
28
29/// The availability zones a managed cluster's replicas are provisioned across,
30/// in configured *provisioning order*.
31///
32/// The order is significant to provisioning: the orchestrator round-robins
33/// replica placement across the list, so the first configured zone is filled
34/// first. Equality is therefore structural and order-sensitive, and the order is
35/// part of the [`ExpectedClusterState`] compare-and-append witness. To compare
36/// two configurations as unordered *pools* of zones, ignoring placement order,
37/// convert with [`AvailabilityZones::pool`].
38#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct AvailabilityZones(pub Vec<String>);
40
41impl AvailabilityZones {
42    /// This configuration as an unordered [`AvailabilityZonePool`]: the same
43    /// zones compared by membership rather than provisioning order. Two replicas
44    /// drawing from the same pool are interchangeable however the lists were
45    /// ordered.
46    pub fn pool(&self) -> AvailabilityZonePool {
47        AvailabilityZonePool(self.0.iter().cloned().collect())
48    }
49}
50
51/// An unordered set of availability zones: an [`AvailabilityZones`] provisioning
52/// list reduced to membership. Produced by [`AvailabilityZones::pool`] for the
53/// one comparison that must ignore order, replica interchangeability.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct AvailabilityZonePool(pub BTreeSet<String>);
56
57/// The config dimensions that distinguish one replica from another. Two replicas
58/// with equal shape are interchangeable. An `AVAILABILITY ZONES` difference is a
59/// shape difference, but only as an unordered pool: reordering the same zones
60/// does not change a replica's shape (see [`ReplicaShape::matches`]).
61#[derive(Clone, Debug)]
62pub struct ReplicaShape {
63    pub size: String,
64    pub availability_zones: AvailabilityZones,
65    pub logging: ComputeReplicaLogging,
66}
67
68impl ReplicaShape {
69    /// Whether two shapes are interchangeable. Availability zones are compared as
70    /// unordered [pools](AvailabilityZones::pool): a replica already placed
71    /// satisfies a desired shape with the same zones in a different order, so a
72    /// mere reorder does not force a reprovision.
73    pub fn matches(&self, other: &ReplicaShape) -> bool {
74        self.size == other.size
75            && self.logging == other.logging
76            && self.availability_zones.pool() == other.availability_zones.pool()
77    }
78}
79
80/// Compare-and-append witness over a managed cluster's durable config: the
81/// fields a conditional write is conditioned on. The applier applies the write
82/// only if the cluster's current config still projects to an equal witness.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct ExpectedClusterState {
85    pub size: String,
86    pub replication_factor: u32,
87    pub availability_zones: AvailabilityZones,
88    pub logging: ComputeReplicaLogging,
89    pub reconfiguration: Option<ReconfigurationRecord>,
90    pub burst: Option<BurstRecord>,
91}
92
93/// An in-flight graceful reconfiguration record, mirrored from durable state.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct ReconfigurationRecord {
96    pub target: ReconfigurationTarget,
97    pub deadline: Timestamp,
98}
99
100/// The full config shape a reconfiguration is moving the cluster to. Distinct
101/// from a replica shape because it additionally carries `replication_factor`, a
102/// cluster-level rather than replica-level dimension.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct ReconfigurationTarget {
105    pub size: String,
106    pub replication_factor: u32,
107    pub availability_zones: AvailabilityZones,
108    pub logging: ComputeReplicaLogging,
109}
110
111/// An active hydration-burst record, mirrored from durable state.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct BurstRecord {
114    pub burst_size: String,
115    pub linger_duration: Duration,
116    pub steady_hydrated_at: Option<Timestamp>,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn azs(zones: &[&str]) -> AvailabilityZones {
124        AvailabilityZones(zones.iter().map(|z| z.to_string()).collect())
125    }
126
127    fn shape(zones: &[&str]) -> ReplicaShape {
128        ReplicaShape {
129            size: "small".to_string(),
130            availability_zones: azs(zones),
131            logging: ComputeReplicaLogging::default(),
132        }
133    }
134
135    #[mz_ore::test]
136    fn availability_zones_identity_is_ordered_pool_is_not() {
137        // Provisioning order is part of the value's identity, so a reorder is a
138        // distinct configuration that the compare-and-append witness can see.
139        assert_ne!(azs(&["a", "b"]), azs(&["b", "a"]));
140        // The pool drops the order: the same zones in any order are one pool, but
141        // a different set of zones is a different pool.
142        assert_eq!(azs(&["a", "b"]).pool(), azs(&["b", "a"]).pool());
143        assert_ne!(azs(&["a", "b"]).pool(), azs(&["a", "c"]).pool());
144    }
145
146    #[mz_ore::test]
147    fn replica_shape_matches_ignores_zone_order() {
148        // Interchangeability ignores provisioning order: an already-placed
149        // replica satisfies a desired shape with the same zones reordered, so a
150        // reorder alone never forces a reprovision.
151        assert!(shape(&["a", "b"]).matches(&shape(&["b", "a"])));
152        // A different zone set is a different shape.
153        assert!(!shape(&["a", "b"]).matches(&shape(&["a", "c"])));
154    }
155}