Skip to main content

krabka_raft/kraft/controller/
activation.rs

1//! Controller activation: the records that a new leader writes to a metadata
2//! log that holds no `metadata.version`.
3//!
4//! Kafka's active controller writes them in its `CompleteActivationEvent`,
5//! with `ActivationRecordsGenerator.recordsForEmptyLog`. They are the
6//! bootstrap records, from the bootstrap checkpoint of a dynamic format or
7//! else from the `bootstrap.checkpoint` of a static one, and the cluster-level
8//! `min.insync.replicas` when the bootstrap records enable ELR. A broker never
9//! writes them. Every replica, a broker-only observer too, applies them from
10//! the log. A controller that cannot write them stops over a fatal fault, as
11//! Kafka's `fatalFaultHandler` halts the process.
12
13use {
14    super::Engine,
15    crate::error::RaftError,
16    krabka_metadata::{
17        BrokerConfigRecord, DEFAULT_BROKER_CONFIG_NODE_ID, MetadataRecord,
18        metadata_version::{ELR_VERSION_FEATURE, METADATA_VERSION_FEATURE},
19    },
20    tokio::sync::oneshot,
21};
22
23/// Kafka's `TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG`.
24const MIN_IN_SYNC_REPLICAS_CONFIG: &str = "min.insync.replicas";
25
26/// The `failureMessage` Kafka's `QuorumController` gives its fatal fault
27/// handler when the activation records cannot be written.
28const ACTIVATION_FAULT: &str = "exception while completing controller activation";
29
30/// What a new leader of an empty metadata log writes: the inputs of Kafka's
31/// `ActivationRecordsGenerator.recordsForEmptyLog`.
32#[derive(Debug, Clone, PartialEq, krabka_macros::FieldDefaults)]
33pub struct Activation {
34    /// The bootstrap records. An empty list writes nothing.
35    pub bootstrap_records: Vec<MetadataRecord>,
36    /// This node's static `min.insync.replicas`: Kafka's
37    /// `ConfigurationControlManager.getStaticallyConfiguredMinInsyncReplicas`.
38    /// It becomes the cluster-level `min.insync.replicas` when the bootstrap
39    /// records enable ELR. Default: `1`, Kafka's default.
40    #[default(1)]
41    pub default_min_insync_replicas: i32,
42}
43
44/// The level of the last `FeatureLevelRecord` for `name` in `records`, or
45/// `None` when they hold none. The last record wins, as it does in Kafka's
46/// `BootstrapMetadata` and on replay.
47fn bootstrap_feature_level(records: &[MetadataRecord], name: &str) -> Option<i16> {
48    records.iter().rev().find_map(|record| match record {
49        MetadataRecord::V1FeatureLevel(feature) if feature.name == name => Some(feature.level),
50        _ => None,
51    })
52}
53
54/// The `metadata.version` level of the last `FeatureLevelRecord` for it in
55/// `records`, or `None` when they hold none.
56pub(super) fn bootstrap_metadata_version(records: &[MetadataRecord]) -> Option<i16> {
57    bootstrap_feature_level(records, METADATA_VERSION_FEATURE)
58}
59
60/// The records of the activation of an empty log, in Kafka's order: the
61/// bootstrap records, and then the cluster-level `min.insync.replicas` at the
62/// static value when the bootstrap records finalize
63/// `eligible.leader.replicas.version` above 0. They go to the log as one
64/// batch, which commits whole, as Kafka writes them in one atomic batch or
65/// metadata transaction.
66pub(super) fn activation_records(activation: &Activation) -> Vec<MetadataRecord> {
67    let mut records = activation.bootstrap_records.clone();
68    if bootstrap_feature_level(&records, ELR_VERSION_FEATURE).is_some_and(|level| level > 0) {
69        records.push(MetadataRecord::V1BrokerConfig(BrokerConfigRecord {
70            node_id: DEFAULT_BROKER_CONFIG_NODE_ID,
71            config_name: MIN_IN_SYNC_REPLICAS_CONFIG.to_owned(),
72            config_value: Some(activation.default_min_insync_replicas.to_string()),
73        }));
74    }
75    records
76}
77
78/// Refuse bootstrap metadata that holds records but does not finalize
79/// `metadata.version`, as Kafka's `BootstrapMetadata` does.
80///
81/// The leader writes the bootstrap records to a log that holds no
82/// `metadata.version`. Records that do not finalize one would leave the log
83/// in that state, and the next leader would write them again.
84///
85/// # Errors
86///
87/// Returns [`RaftError::Startup`] when `records` is not empty and holds no
88/// `FeatureLevelRecord` for `metadata.version`, with the message of Kafka's
89/// `BootstrapMetadata.fromRecords`. It also returns it when the last such
90/// record sets level 0, which removes the feature and which no
91/// `MetadataVersion` has.
92pub(super) fn check_bootstrap_records(
93    records: &[MetadataRecord],
94    source: &str,
95) -> Result<(), RaftError> {
96    if records.is_empty() {
97        return Ok(());
98    }
99    match bootstrap_metadata_version(records) {
100        Some(level) if level > 0 => Ok(()),
101        Some(level) => Err(RaftError::Startup(format!(
102            "No MetadataVersion with feature level {level} in the bootstrap metadata from \
103             {source}"
104        ))),
105        None => Err(RaftError::Startup(format!(
106            "No FeatureLevelRecord for {METADATA_VERSION_FEATURE} was found in the bootstrap \
107             metadata from {source}"
108        ))),
109    }
110}
111
112impl Engine {
113    /// Write the activation records when the log of this new leader holds no
114    /// `metadata.version`: Kafka's `CompleteActivationEvent`.
115    ///
116    /// The leader calls this directly after it appends the `LeaderChange`
117    /// batch of its epoch. So the records come before every other write of
118    /// the epoch, as Kafka's activation event is the first event of the
119    /// active controller. The log holds `metadata.version` when the committed
120    /// image finalizes it. It also holds it when a batch that an earlier
121    /// leader appended and did not commit finalizes it, because that batch
122    /// commits with this epoch. Kafka makes the same decision after its
123    /// controller has replayed the log up to the epoch start.
124    ///
125    /// The records go through [`Self::on_activation_change`], so the leader
126    /// validates and encodes them as it does every other write. Nothing waits
127    /// for them to commit. A leader that refuses them, or that cannot read
128    /// its own log tail to decide, sets [`Engine::activation_fault`]. The
129    /// engine then stops, and the controller stops over the fault, as Kafka's
130    /// `QuorumController` gives the failure to its `fatalFaultHandler`.
131    pub fn complete_activation(&mut self) {
132        let Some(level) = bootstrap_metadata_version(&self.activation.bootstrap_records)
133            .filter(|level| *level > 0)
134        else {
135            return;
136        };
137        match self.replay_earlier_epoch_tail(|_, _| {}) {
138            Ok(image) if image.finalized_metadata_version().is_some() => return,
139            Ok(_) => {}
140            Err(error) => {
141                self.activation_fault = Some(format!("{ACTIVATION_FAULT}: {error}"));
142                return;
143            }
144        }
145        let records = activation_records(&self.activation);
146        tracing::info!(
147            records = records.len(),
148            metadata_version = level,
149            "kraft: performing controller activation. The metadata log appears to be empty: \
150             appending the bootstrap records"
151        );
152        let (reply, mut outcome) = oneshot::channel();
153        self.on_activation_change(&records, reply);
154        if let Ok(Err(error)) = outcome.try_recv() {
155            self.activation_fault = Some(format!("{ACTIVATION_FAULT}: {error}"));
156        }
157    }
158}