krabka_metadata/krabka_version.rs
1//! The krabka-owned `krabka.version` feature (KIP-584).
2//!
3//! From krabka-broker 1.0.0 on, `krabka.version` gates every krabka-only
4//! on-disk and inter-node format change, as `metadata.version` does for
5//! Kafka's. `metadata.version` mirrors Kafka's `MetadataVersion` table level
6//! for level, so krabka cannot add a level there. A broker keeps writing and
7//! sending the old format until the operator finalizes the `krabka.version`
8//! level that introduces the new one.
9//!
10//! # Levels
11//!
12//! - **Level 0** means exactly the krabka-broker 1.0.0 formats. An image that
13//! has no finalized `krabka.version` reads as level 0, as Kafka treats an
14//! absent feature (see [`KrabkaVersion::finalized`]).
15//! - **Level 1** also means exactly the 1.0.0 formats. It carries no format
16//! change, and it never will: a 1.0.0 node advertises support for it, so a
17//! later release cannot give it a meaning that 1.0.0 nodes do not know. The
18//! first krabka-only format change takes level 2.
19//! - **Level 2** turns on diskless WAL object reservations
20//! ([`DISKLESS_OBJECT_RESERVATION_LEVEL`]). A broker records a reservation
21//! for a WAL object in `__diskless_wal_index` before it uploads the object,
22//! and clears it once the object's index records are applied, so no broker
23//! reclaims an object whose index publication is still in flight. The
24//! reservation reuses the 1.x delete-floor record encoding, so every 1.x
25//! reader can still replay it. Below level 2 a broker writes no
26//! reservations. Level 2 changes no private RPC version.
27//! - **Level 3** turns on two formats. Level 2 shipped in krabka-broker 1.1.3,
28//! so a later format cannot join it.
29//! - The OCSF 1.3.0 activity ids of `__krabka_audit` records
30//! ([`OCSF_ACTIVITY_IDS_LEVEL`]). Application Lifecycle reports a start as
31//! activity 3 (Start) and a stop as 4 (Stop), Authorize Session reports an
32//! ACL denial as 99 (Other), API Activity reports an admin operation as
33//! Create, Read, Update or Delete where one applies, and a record carries
34//! `metadata.log_version` `"2"`. Below level 3 a broker writes the 1.0.0
35//! shape. `krabka-audit verify` hashes each record's bytes, so a chain
36//! that mixes both shapes still verifies.
37//! - The normalized tiered-storage key prefix
38//! ([`TIERED_KEY_PREFIX_NORMALIZED_LEVEL`]). A broker trims spaces and
39//! slashes from the configured prefix before it builds a segment key, as
40//! it already does for diskless WAL objects. Below level 3 it writes the
41//! key with the prefix exactly as configured. A reader looks a segment up
42//! under both forms at every level.
43//!
44//! Level 3 changes no private RPC version.
45//!
46//! # Why the maximum is 1, not 0
47//!
48//! Kafka does not advertise a feature whose highest supported level is 0.
49//! `BrokerFeatures.defaultSupportedFeatures` and
50//! `QuorumFeatures.defaultSupportedFeatureMap` both add a feature to the
51//! supported map only when `maxVersion > 0`, and the controller treats a
52//! feature that a node does not advertise as supported at `[0, 0]`. A feature
53//! with a maximum of 0 therefore cannot be finalized above 0, and
54//! `kafka-features describe`, which lists `supportedFeatures`, would show it
55//! only because krabka breaks that rule. With a maximum of 1, `krabka.version`
56//! appears in `kafka-features describe`, and
57//! `kafka-features upgrade --feature krabka.version=1` finalizes it.
58//! `kafka-features upgrade --release-version` iterates Kafka's own
59//! `Feature.PRODUCTION_FEATURES` only, so it never touches `krabka.version`.
60//!
61//! # Why a fresh cluster bootstraps at 0
62//!
63//! A fresh cluster bootstraps `krabka.version` at [`KRABKA_VERSION_MIN`],
64//! whatever the bootstrap `metadata.version` is, so `krabka format` writes no
65//! `FeatureLevelRecord` for it, as Kafka writes none for a feature at level 0.
66//! An Apache Kafka node in the same cluster does not know `krabka.version`, so
67//! it treats the feature as supported at `[0, 0]` (`QuorumFeatures.DISABLED`).
68//! A Kafka controller that replays a finalized level above 0 halts: it throws
69//! "this controller only supports versions 0", and its
70//! `ProcessTerminatingFaultHandler` exits. A Kafka broker cannot register with
71//! a controller that has finalized a level above 0.
72//!
73//! This departs from Kafka's rule for its own production features, which
74//! bootstrap at their latest level. That rule holds because every Kafka node
75//! of the release knows those features. `krabka.version` is not one of Kafka's
76//! features, so no Kafka node ever knows it. Nothing is lost: levels 0 and 1
77//! mean the same formats.
78//!
79//! An operator finalizes `krabka.version` with
80//! `kafka-features upgrade --feature krabka.version=1` once every node supports
81//! it. The controller refuses a level that some registered node does not
82//! support, and a Kafka node, which advertises nothing for the feature,
83//! supports only 0.
84//!
85//! # Dependencies
86//!
87//! `krabka.version` declares no KIP-1022 dependency on `metadata.version`. A
88//! future level that needs a Kafka record shape adds one for that level only.
89//!
90//! # Private-RPC version negotiation
91//!
92//! [`private_rpc_version`] is the negotiation mechanism for the krabka-private
93//! controller RPCs, the same one Kafka uses for inter-broker protocol versions
94//! through `metadata.version`. The controller finalizes a level only once every
95//! registered node advertises support for it. The finalized level therefore
96//! tells a sender which request versions every peer serves, and a sender never
97//! sends a version above the one the table gives for that level.
98
99use crate::MetadataImage;
100
101/// KIP-584 feature name.
102pub const KRABKA_VERSION_FEATURE: &str = "krabka.version";
103
104/// Minimum supported level: the krabka-broker 1.0.0 formats.
105pub const KRABKA_VERSION_MIN: i16 = 0;
106
107/// Maximum supported level, and the latest production level. Level 1 means
108/// the same formats as level 0. See the module docs for the meaning of each
109/// level.
110pub const KRABKA_VERSION_MAX: i16 = 3;
111
112/// The first level at which a broker writes diskless WAL object reservations
113/// before it uploads an object. See the module docs.
114pub const DISKLESS_OBJECT_RESERVATION_LEVEL: i16 = 2;
115
116/// The first level at which a broker writes `__krabka_audit` records with the
117/// OCSF 1.3.0 activity ids. See the module docs.
118pub const OCSF_ACTIVITY_IDS_LEVEL: i16 = 3;
119
120/// The first level at which a broker normalizes the configured tiered-storage
121/// prefix before it builds a segment key. See the module docs.
122pub const TIERED_KEY_PREFIX_NORMALIZED_LEVEL: i16 = 3;
123
124/// The finalized `krabka.version` level of a metadata image.
125#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
126pub struct KrabkaVersion(i16);
127
128impl KrabkaVersion {
129 /// Wrap a raw level.
130 #[must_use]
131 pub const fn new(level: i16) -> Self {
132 Self(level)
133 }
134
135 /// The finalized `krabka.version` of `image`. An image that has not
136 /// finalized the feature reads as level 0, as Kafka treats an absent
137 /// feature.
138 #[must_use]
139 pub fn finalized(image: &MetadataImage) -> Self {
140 Self(
141 image
142 .finalized_feature(KRABKA_VERSION_FEATURE)
143 .unwrap_or(KRABKA_VERSION_MIN),
144 )
145 }
146
147 /// The raw level.
148 #[must_use]
149 pub const fn level(self) -> i16 {
150 self.0
151 }
152
153 /// The highest request version of `rpc` that a node may send at this
154 /// level. See [`private_rpc_version`].
155 #[must_use]
156 pub const fn max_request_version(self, rpc: PrivateRpc) -> Option<i16> {
157 rpc.max_request_version(self.0)
158 }
159}
160
161/// A krabka-private controller RPC. Its api key sits at 1000 or above, so a
162/// later Kafka assignment cannot collide with it.
163#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
164pub enum PrivateRpc {
165 /// `SubmitChange`, api key 1003: a node forwards a metadata change to the
166 /// controller leader.
167 SubmitChange,
168 /// `MetadataFetch`, api key 1004: an observer fetches the metadata log.
169 MetadataFetch,
170 /// `DelegationTokenMutation`, api key 1005: a guarded forward of a
171 /// delegation-token change.
172 DelegationTokenMutation,
173}
174
175impl PrivateRpc {
176 /// Every private RPC, in api-key order.
177 pub const ALL: [Self; 3] = [
178 Self::SubmitChange,
179 Self::MetadataFetch,
180 Self::DelegationTokenMutation,
181 ];
182
183 /// The api key of this RPC.
184 #[must_use]
185 pub const fn api_key(self) -> i16 {
186 match self {
187 Self::SubmitChange => 1003,
188 Self::MetadataFetch => 1004,
189 Self::DelegationTokenMutation => 1005,
190 }
191 }
192
193 /// The private RPC with `api_key`, or `None` for any other key.
194 #[must_use]
195 pub const fn from_api_key(api_key: i16) -> Option<Self> {
196 match api_key {
197 1003 => Some(Self::SubmitChange),
198 1004 => Some(Self::MetadataFetch),
199 1005 => Some(Self::DelegationTokenMutation),
200 _ => None,
201 }
202 }
203
204 /// The highest request version of this RPC that a node may send when the
205 /// finalized `krabka.version` is `krabka_version`. `None` for a level
206 /// outside `[KRABKA_VERSION_MIN, KRABKA_VERSION_MAX]`: no node of this
207 /// build can see such a level finalized, because the controller finalizes
208 /// only a level that every node supports.
209 #[must_use]
210 pub const fn max_request_version(self, krabka_version: i16) -> Option<i16> {
211 if krabka_version < KRABKA_VERSION_MIN || krabka_version > KRABKA_VERSION_MAX {
212 return None;
213 }
214 // Levels 0 and 1 are both the 1.0.0 formats, and levels 2 and 3
215 // change no RPC, so every RPC is v0. A level that raises an RPC's version adds
216 // an arm here.
217 Some(match self {
218 Self::SubmitChange | Self::MetadataFetch | Self::DelegationTokenMutation => 0,
219 })
220 }
221}
222
223/// The highest request version of the private controller RPC `api_key` that a
224/// node may send when the finalized `krabka.version` is `krabka_version`.
225///
226/// Returns `None` for an api key that is not a private controller RPC, and for
227/// a level this build does not support. At levels 0 to 3 every RPC is v0.
228#[must_use]
229pub const fn private_rpc_version(api_key: i16, krabka_version: i16) -> Option<i16> {
230 match PrivateRpc::from_api_key(api_key) {
231 Some(rpc) => rpc.max_request_version(krabka_version),
232 None => None,
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use assert2::check;
239
240 use super::*;
241 use crate::{FeatureLevelRecord, MetadataRecord};
242
243 fn image_at(level: Option<i16>) -> MetadataImage {
244 let mut image = MetadataImage::new(uuid::Uuid::nil());
245 if let Some(level) = level {
246 image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
247 name: KRABKA_VERSION_FEATURE.into(),
248 level,
249 }));
250 }
251 image
252 }
253
254 #[test]
255 fn constants_are_pinned() {
256 check!(
257 (
258 KRABKA_VERSION_FEATURE,
259 KRABKA_VERSION_MIN,
260 KRABKA_VERSION_MAX,
261 DISKLESS_OBJECT_RESERVATION_LEVEL,
262 OCSF_ACTIVITY_IDS_LEVEL,
263 TIERED_KEY_PREFIX_NORMALIZED_LEVEL,
264 ) == ("krabka.version", 0, 3, 2, 3, 3)
265 );
266 }
267
268 #[test]
269 fn finalized_reads_the_image() {
270 for (case, finalized, want) in [
271 ("absent reads as 0", None, KrabkaVersion::new(0)),
272 // A level-0 record deletes the entry, so it reads as absent.
273 ("explicit 0", Some(0), KrabkaVersion::new(0)),
274 (
275 "maximum",
276 Some(KRABKA_VERSION_MAX),
277 KrabkaVersion::new(KRABKA_VERSION_MAX),
278 ),
279 ] {
280 check!(
281 KrabkaVersion::finalized(&image_at(finalized)) == want,
282 "{case}"
283 );
284 }
285 }
286
287 #[test]
288 fn finalized_ignores_other_features() {
289 let mut image = image_at(None);
290 image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
291 name: "transaction.version".into(),
292 level: 2,
293 }));
294 check!(KrabkaVersion::finalized(&image).level() == 0);
295 }
296
297 /// One row per private RPC. The match has no wildcard arm, so a new
298 /// `PrivateRpc` variant does not compile until it has a row here.
299 #[test]
300 fn private_rpc_version_table() {
301 for rpc in PrivateRpc::ALL {
302 // (api key, [(krabka.version, highest request version)])
303 let (api_key, rows): (i16, [(i16, Option<i16>); 6]) = match rpc {
304 PrivateRpc::SubmitChange => (
305 1003,
306 [
307 (-1, None),
308 (0, Some(0)),
309 (1, Some(0)),
310 (2, Some(0)),
311 (3, Some(0)),
312 (4, None),
313 ],
314 ),
315 PrivateRpc::MetadataFetch => (
316 1004,
317 [
318 (-1, None),
319 (0, Some(0)),
320 (1, Some(0)),
321 (2, Some(0)),
322 (3, Some(0)),
323 (4, None),
324 ],
325 ),
326 PrivateRpc::DelegationTokenMutation => (
327 1005,
328 [
329 (-1, None),
330 (0, Some(0)),
331 (1, Some(0)),
332 (2, Some(0)),
333 (3, Some(0)),
334 (4, None),
335 ],
336 ),
337 };
338 check!(rpc.api_key() == api_key);
339 check!(PrivateRpc::from_api_key(api_key) == Some(rpc));
340 for (level, want) in rows {
341 check!(
342 private_rpc_version(api_key, level) == want,
343 "{rpc:?} at {level}"
344 );
345 check!(
346 KrabkaVersion::new(level).max_request_version(rpc) == want,
347 "{rpc:?} at {level}"
348 );
349 }
350 }
351 }
352
353 #[test]
354 fn private_rpc_version_refuses_other_api_keys() {
355 for api_key in [0, 18, 1000, 1001, 1002, 1006, -1] {
356 check!(
357 private_rpc_version(api_key, 0).is_none(),
358 "api key {api_key}"
359 );
360 }
361 }
362}