krabka_raft/config/routing.rs
1//! The request-routing seams the broker crate hangs on a controller.
2//!
3//! `RaftShardRouter` claims KIP-595 traffic addressed to a non-metadata quorum
4//! shard before metadata dispatch sees it, and `ControllerAdminRouter` carries
5//! the KIP-919 Admin surface, so a request the controller listener accepts is
6//! served by the broker's own handler registry rather than by a second
7//! implementation of the same semantics. Both are hooks the broker installs on
8//! `ControllerConfig` rather than settings an operator writes down, which is
9//! why they sit apart from the configuration itself.
10
11use {
12 crate::{config::kafka_4_3_1_max, error::RaftError},
13 bytes::Bytes,
14 krabka_metadata::{
15 MetadataRecord,
16 metadata_version::{
17 CORDONED_LOG_DIRS_MIN_LEVEL, METADATA_VERSION_FEATURE, SHARE_VERSION_FEATURE,
18 },
19 },
20 krabka_protocol::ProtocolRequest,
21 std::{collections::BTreeMap, future::Future, net::SocketAddr, pin::Pin},
22};
23
24/// Optional router for KIP-595 traffic addressed to non-metadata quorum shards.
25pub type ShardRouteFuture<'a> =
26 Pin<Box<dyn Future<Output = Result<Option<Bytes>, RaftError>> + Send + 'a>>;
27
28/// Classifies and serves shard-addressed KIP-595 requests before metadata dispatch.
29pub trait RaftShardRouter: Send + Sync {
30 fn route(
31 &self,
32 api_key: i16,
33 body: Bytes,
34 principal: Option<&krabka_security::Principal>,
35 ) -> ShardRouteFuture<'_>;
36}
37
38/// Whether a listener advertises and accepts what Kafka trunk serves beyond
39/// the latest Kafka release.
40///
41/// This is Kafka's internal `unstable.api.versions.enable` broker config.
42/// Kafka's `ApiKeys.toApiVersion` advertises `latestVersion(false)` unless it
43/// is set, and `ApiKeys.isVersionEnabled` refuses the unstable version on
44/// receive, which closes the connection. The default is
45/// [`Disabled`][Self::Disabled], as it is in Kafka.
46///
47/// krabka vendors Kafka trunk's schemas, so while the setting is
48/// [`Disabled`][Self::Disabled] a listener also answers exactly as a Kafka
49/// 4.3.1 broker does: each API is capped at the version that release
50/// advertises, and an api key that release does not have is neither
51/// advertised nor accepted. [`Enabled`][Self::Enabled] serves what krabka
52/// implements from trunk.
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54pub enum UnstableApiVersions {
55 /// Advertise and accept exactly Kafka 4.3.1's API versions.
56 #[default]
57 Disabled,
58 /// Advertise and accept each API up to the highest version it decodes.
59 Enabled,
60}
61
62impl From<bool> for UnstableApiVersions {
63 fn from(enabled: bool) -> Self {
64 if enabled {
65 Self::Enabled
66 } else {
67 Self::Disabled
68 }
69 }
70}
71
72/// Whether a node supports feature levels past the latest production ones.
73///
74/// This is Kafka's internal `unstable.feature.versions.enable` broker config.
75/// Kafka's `BrokerFeatures.defaultSupportedFeatures` and
76/// `QuorumFeatures.defaultSupportedFeatureMap` cap every feature at its
77/// `latestProduction` level unless it is set, and `kafka-storage format`
78/// refuses an unstable `metadata.version`. In Kafka 4.3.1 the one feature that
79/// has such levels is `metadata.version`, whose latest production level is
80/// `4.3-IV0` (30); krabka also knows trunk's `4.4-IV0` to `4.5-IV0` (31-34).
81#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
82pub enum UnstableFeatureVersions {
83 /// Support each feature up to its latest production level.
84 #[default]
85 Disabled,
86 /// Support each feature up to the highest level krabka knows.
87 Enabled,
88}
89
90impl From<bool> for UnstableFeatureVersions {
91 fn from(enabled: bool) -> Self {
92 if enabled {
93 Self::Enabled
94 } else {
95 Self::Disabled
96 }
97 }
98}
99
100/// Kafka 4.3's `MetadataVersion.LATEST_PRODUCTION`, `4.3-IV0`.
101///
102/// It is the highest `metadata.version` a node supports while [`UnstableFeatureVersions`] is
103/// [`Disabled`][UnstableFeatureVersions::Disabled], and the level a cluster
104/// bootstraps at when no release is named.
105pub const LATEST_PRODUCTION_METADATA_VERSION: i16 = CORDONED_LOG_DIRS_MIN_LEVEL;
106
107/// The latest production level of each feature that has levels past it, as
108/// Kafka's `Feature.latestProduction` gives it: what a node supports unless
109/// [`UnstableFeatureVersions`] is [`Enabled`][UnstableFeatureVersions::Enabled].
110/// A feature that is not listed has no such level.
111///
112/// In Kafka 4.3.1 that is `metadata.version`, whose trunk levels are `4.4-IV0`
113/// and above, and `share.version`, whose level 2 is trunk's `SV_2`
114/// (KIP-1191).
115const LATEST_PRODUCTION_LEVELS: [ProductionLevel; 2] = [
116 ProductionLevel {
117 feature: METADATA_VERSION_FEATURE,
118 level: LATEST_PRODUCTION_METADATA_VERSION,
119 },
120 ProductionLevel {
121 feature: SHARE_VERSION_FEATURE,
122 level: 1,
123 },
124];
125
126/// The latest production level of one feature.
127struct ProductionLevel {
128 feature: &'static str,
129 level: i16,
130}
131
132/// The supported range of `feature` under `unstable`.
133///
134/// It is the `krabka_metadata` registry's range, which lists the levels of Kafka trunk, capped at the
135/// feature's latest production level (see `LATEST_PRODUCTION_LEVELS`) unless
136/// unstable feature versions are enabled.
137///
138/// A default node therefore advertises the ranges of Kafka 4.3.1.
139#[must_use]
140pub fn supported_feature_range(
141 feature: &dyn krabka_metadata::Feature,
142 unstable: UnstableFeatureVersions,
143) -> (i16, i16) {
144 let (min, max) = feature.supported_range();
145 match LATEST_PRODUCTION_LEVELS
146 .iter()
147 .find(|production| production.feature == feature.name())
148 {
149 Some(production) if unstable == UnstableFeatureVersions::Disabled => {
150 (min, max.min(production.level))
151 }
152 _ => (min, max),
153 }
154}
155
156/// The feature ranges a node registers with the controller under `unstable`.
157///
158/// This is Kafka's `BrokerFeatures.createDefault(unstableFeatureVersionsEnabled)`:
159/// [`krabka_metadata::supported_feature_ranges`] with each registry feature
160/// capped by [`supported_feature_range`].
161#[must_use]
162pub fn supported_feature_ranges(unstable: UnstableFeatureVersions) -> BTreeMap<String, (i16, i16)> {
163 let mut ranges = krabka_metadata::supported_feature_ranges();
164 for feature in krabka_metadata::feature_registry() {
165 ranges.insert(
166 feature.name().to_owned(),
167 supported_feature_range(*feature, unstable),
168 );
169 }
170 ranges
171}
172
173/// One Kafka API version range served by a controller-listener Admin router.
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175pub struct ControllerApiVersion {
176 pub api_key: i16,
177 pub min_version: i16,
178 /// The highest version the listener decodes, the generated `MAX_VERSION`.
179 pub max_version: i16,
180 /// The highest version a Kafka 4.3.1 listener advertises by default, or
181 /// `None` when that release has no such api key. It is below
182 /// `max_version` for a version Kafka trunk added, or one 4.3.1 marks
183 /// `latestVersionUnstable`.
184 pub released_max: Option<i16>,
185 pub flexible_min: i16,
186}
187
188impl ControllerApiVersion {
189 /// The request schema's full decodable range and flexible-header boundary,
190 /// with the default maximum taken from Kafka 4.3.1's advertised range.
191 #[must_use]
192 pub const fn for_request<R: ProtocolRequest>() -> Self {
193 Self {
194 api_key: R::API_KEY,
195 min_version: R::MIN_VERSION,
196 max_version: R::MAX_VERSION,
197 released_max: kafka_4_3_1_max(R::API_KEY),
198 flexible_min: R::FLEXIBLE_MIN,
199 }
200 }
201
202 /// The highest version the listener advertises and accepts under
203 /// `unstable`, or `None` when it neither advertises nor accepts the api
204 /// key at all.
205 #[must_use]
206 pub const fn enabled_max(self, unstable: UnstableApiVersions) -> Option<i16> {
207 match unstable {
208 UnstableApiVersions::Enabled => Some(self.max_version),
209 UnstableApiVersions::Disabled => self.released_max,
210 }
211 }
212
213 /// Whether `version` is one that `unstable` disables: inside the
214 /// decodable range, above the enabled maximum. Kafka's
215 /// `Processor.parseRequestHeader` closes the connection on such a request,
216 /// and on any request for an api key it does not know.
217 #[must_use]
218 pub const fn is_disabled_version(self, version: i16, unstable: UnstableApiVersions) -> bool {
219 let above_enabled = match self.enabled_max(unstable) {
220 Some(max) => version > max,
221 None => true,
222 };
223 above_enabled && version <= self.max_version
224 }
225}
226
227/// Authenticated request handed from the controller listener to the broker's
228/// existing Admin handler registry.
229#[derive(Clone, Debug)]
230pub struct ControllerAdminRequest {
231 pub api_key: i16,
232 pub api_version: i16,
233 pub correlation_id: i32,
234 pub client_id: Option<String>,
235 pub body: Bytes,
236 pub peer: SocketAddr,
237 pub principal: Option<krabka_security::Principal>,
238 pub authenticated_via_token: bool,
239}
240
241/// Encoded Kafka response body plus its response-header shape.
242#[derive(Clone, Debug, Eq, PartialEq)]
243pub struct ControllerAdminResponse {
244 pub body: Bytes,
245 pub flexible: bool,
246}
247
248pub type ControllerAdminRouteFuture<'a> =
249 Pin<Box<dyn Future<Output = Result<Option<ControllerAdminResponse>, RaftError>> + Send + 'a>>;
250
251/// Optional KIP-919 Admin RPC surface attached by the broker crate.
252pub trait ControllerAdminRouter: Send + Sync {
253 fn api_versions(&self) -> &[ControllerApiVersion];
254 fn route(&self, request: ControllerAdminRequest) -> ControllerAdminRouteFuture<'_>;
255}
256
257/// The batch [`RegistrationAdmission::admit`] answers with.
258pub type RegistrationAdmissionFuture<'a> =
259 Pin<Box<dyn Future<Output = Result<Vec<MetadataRecord>, RaftError>> + Send + 'a>>;
260
261/// What the broker crate does to a batch that registers a broker anew, before
262/// the leader appends it.
263///
264/// Kafka's `ClusterControlManager.registerBroker` writes the records that
265/// retire the previous incarnation of a broker id -- the partition changes of
266/// `ReplicationControlManager.handleBrokerShutdown` -- ahead of the new
267/// `RegisterBrokerRecord`, in the same batch. Those changes depend on the
268/// heartbeat registry and the election ladder, which live in the broker crate.
269/// The leader hands every batch that carries a new registration (a
270/// `V1BrokerRegistration` with no epoch yet) to this hook, whether it was
271/// submitted on this node or forwarded by a follower, and appends the batch the
272/// hook returns.
273pub trait RegistrationAdmission: Send + Sync {
274 fn admit(&self, records: Vec<MetadataRecord>) -> RegistrationAdmissionFuture<'_>;
275}
276
277#[cfg(test)]
278mod tests {
279 use {super::*, assert2::assert};
280
281 /// A registry feature with a chosen name and range.
282 struct Listed {
283 name: &'static str,
284 min: i16,
285 max: i16,
286 }
287
288 impl krabka_metadata::Feature for Listed {
289 fn name(&self) -> &'static str { self.name }
290
291 fn supported_range(&self) -> (i16, i16) { (self.min, self.max) }
292
293 fn default_level(&self, _bootstrap_mv: i16) -> i16 { self.min }
294 }
295
296 /// The range each node advertises: Kafka 4.3.1's by default, and the
297 /// registry's, which is trunk's, under `unstable.feature.versions.enable`.
298 /// `share.version` 2 is trunk's `SV_2` (KIP-1191).
299 #[test]
300 fn a_default_node_advertises_the_latest_production_ranges() {
301 use {
302 UnstableFeatureVersions::{Disabled, Enabled},
303 krabka_metadata::metadata_version::{METADATA_VERSION_FEATURE, SHARE_VERSION_FEATURE},
304 };
305
306 // (feature, registry range, unstable, expected range)
307 let rows = [
308 (SHARE_VERSION_FEATURE, (0, 1), Disabled, (0, 1)),
309 (SHARE_VERSION_FEATURE, (0, 1), Enabled, (0, 1)),
310 (SHARE_VERSION_FEATURE, (0, 2), Disabled, (0, 1)),
311 (SHARE_VERSION_FEATURE, (0, 2), Enabled, (0, 2)),
312 (METADATA_VERSION_FEATURE, (7, 33), Disabled, (7, 30)),
313 (METADATA_VERSION_FEATURE, (7, 33), Enabled, (7, 33)),
314 ("group.version", (0, 1), Disabled, (0, 1)),
315 ("group.version", (0, 1), Enabled, (0, 1)),
316 ];
317 let actual: Vec<_> = rows
318 .iter()
319 .map(|&(name, range, unstable, _)| {
320 (
321 name,
322 unstable,
323 supported_feature_range(
324 &Listed {
325 name,
326 min: range.0,
327 max: range.1,
328 },
329 unstable,
330 ),
331 )
332 })
333 .collect();
334 let expected: Vec<_> = rows
335 .iter()
336 .map(|&(name, _, unstable, want)| (name, unstable, want))
337 .collect();
338 assert!(actual == expected);
339 }
340
341 /// The whole feature map a node registers with, as Kafka's
342 /// `BrokerFeatures.createDefault` builds it: every registry feature,
343 /// `krabka.version` among them, plus the registration-only KIP-1155
344 /// capability.
345 #[test]
346 fn registration_advertises_every_feature_with_krabka_version() {
347 use {
348 UnstableFeatureVersions::{Disabled, Enabled},
349 krabka_metadata::metadata_version::{METADATA_VERSION_MAX, METADATA_VERSION_MIN},
350 std::collections::BTreeMap,
351 };
352
353 let expected = |metadata_max: i16, share_max: i16| {
354 BTreeMap::from(
355 [
356 ("eligible.leader.replicas.version", (0, 1)),
357 ("group.version", (0, 1)),
358 ("krabka.metadata.downgrade", (1, 1)),
359 ("krabka.version", (0, 2)),
360 ("kraft.version", (0, 1)),
361 ("metadata.version", (METADATA_VERSION_MIN, metadata_max)),
362 ("share.version", (0, share_max)),
363 ("streams.version", (0, 1)),
364 ("transaction.version", (0, 2)),
365 ]
366 .map(|(name, range)| (name.to_owned(), range)),
367 )
368 };
369 let actual = [Disabled, Enabled].map(supported_feature_ranges);
370 assert!(
371 actual
372 == [
373 expected(LATEST_PRODUCTION_METADATA_VERSION, 1),
374 expected(METADATA_VERSION_MAX, 2),
375 ]
376 );
377 }
378}