Skip to main content

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}