1use {
15 crate::kraft::{
16 transport::{ObserverReplica, QuorumStateSnapshot},
17 types::NodeId,
18 },
19 krabka_protocol::{
20 owned::{
21 common::describe_quorum_response::replica_state::ReplicaState,
22 describe_quorum_request::DescribeQuorumRequest,
23 describe_quorum_response::{
24 DescribeQuorumResponse, Listener, Node, PartitionData, TopicData,
25 },
26 },
27 primitives::uuid::Uuid as WireUuid,
28 },
29 std::collections::BTreeMap,
30};
31
32const METADATA_TOPIC: &str = "__cluster_metadata";
33const UNKNOWN_TOPIC_OR_PARTITION: i16 = 3;
34const NOT_LEADER_OR_FOLLOWER: i16 = 6;
35const UNKNOWN_TOPIC_OR_PARTITION_MESSAGE: &str = "This server does not host this topic-partition.";
37const NOT_LEADER_OR_FOLLOWER_MESSAGE: &str = "For requests intended only for the leader, this \
39 error indicates that the broker is not the current leader. For requests intended for any \
40 replica, this error indicates that the broker is not a replica of the topic partition.";
41
42fn node_to_wire(id: NodeId) -> i32 { i32::try_from(id.0).unwrap_or(-1) }
44
45fn not_leader_or_follower_response() -> DescribeQuorumResponse {
46 DescribeQuorumResponse {
47 topics: vec![TopicData {
48 topic_name: METADATA_TOPIC.into(),
49 partitions: vec![PartitionData {
50 partition_index: 0,
51 error_code: NOT_LEADER_OR_FOLLOWER,
52 error_message: Some(NOT_LEADER_OR_FOLLOWER_MESSAGE.into()),
53 ..Default::default()
54 }],
55 ..Default::default()
56 }],
57 error_message: Some(String::new()),
60 ..Default::default()
61 }
62}
63
64fn names_only_the_metadata_partition(request: &DescribeQuorumRequest) -> bool {
66 matches!(
67 request.topics.as_slice(),
68 [topic] if topic.topic_name == METADATA_TOPIC
69 && matches!(topic.partitions.as_slice(), [partition] if partition.partition_index == 0)
70 )
71}
72
73fn observer_state(observer: &ObserverReplica) -> ReplicaState {
76 ReplicaState {
77 replica_id: node_to_wire(observer.id),
78 replica_directory_id: WireUuid(*observer.directory_id.as_bytes()),
79 log_end_offset: observer.log_end_offset,
80 last_fetch_timestamp: observer.last_fetch_ms,
81 last_caught_up_timestamp: observer.last_caught_up_ms,
82 ..Default::default()
83 }
84}
85
86fn replica_state(
89 quorum: &QuorumStateSnapshot,
90 id: NodeId,
91 directory_id: uuid::Uuid,
92) -> ReplicaState {
93 let tracked = |map: &BTreeMap<NodeId, i64>| map.get(&id).copied().unwrap_or(-1);
94 ReplicaState {
95 replica_id: node_to_wire(id),
96 replica_directory_id: WireUuid(*directory_id.as_bytes()),
97 log_end_offset: tracked(&quorum.per_replica_fetch_offset),
98 last_fetch_timestamp: tracked(&quorum.per_replica_last_fetch_ms),
99 last_caught_up_timestamp: tracked(&quorum.per_replica_last_caught_up_ms),
100 ..Default::default()
101 }
102}
103
104#[must_use]
114pub fn describe_quorum(
115 request: &DescribeQuorumRequest,
116 quorum: &QuorumStateSnapshot,
117) -> DescribeQuorumResponse {
118 if !names_only_the_metadata_partition(request) {
119 let topics: Vec<TopicData> = request
120 .topics
121 .iter()
122 .map(|topic| TopicData {
123 topic_name: topic.topic_name.clone(),
124 partitions: topic
125 .partitions
126 .iter()
127 .map(|partition| PartitionData {
128 partition_index: partition.partition_index,
129 error_code: UNKNOWN_TOPIC_OR_PARTITION,
130 error_message: Some(UNKNOWN_TOPIC_OR_PARTITION_MESSAGE.into()),
131 ..Default::default()
132 })
133 .collect(),
134 ..Default::default()
135 })
136 .collect();
137 return DescribeQuorumResponse {
138 topics,
139 error_message: Some(String::new()),
142 ..Default::default()
143 };
144 }
145 let Some(leader) = quorum.leader_id.filter(|_| quorum.is_leader) else {
146 return not_leader_or_follower_response();
147 };
148 DescribeQuorumResponse {
149 topics: vec![TopicData {
150 topic_name: METADATA_TOPIC.into(),
151 partitions: vec![PartitionData {
152 partition_index: 0,
153 leader_id: node_to_wire(leader),
154 leader_epoch: i32::try_from(quorum.leader_epoch).unwrap_or(i32::MAX),
155 high_watermark: quorum.high_watermark,
156 current_voters: quorum
157 .voters
158 .iter()
159 .map(|voter| replica_state(quorum, voter.id, voter.directory_id))
160 .collect(),
161 observers: quorum.observers.iter().map(observer_state).collect(),
162 error_message: Some(String::new()),
165 ..Default::default()
166 }],
167 ..Default::default()
168 }],
169 nodes: quorum
170 .voters
171 .iter()
172 .map(|voter| Node {
173 node_id: node_to_wire(voter.id),
174 listeners: voter
175 .endpoints
176 .iter()
177 .map(|endpoint| Listener {
178 name: endpoint.name.clone(),
179 host: endpoint.host.clone(),
180 port: endpoint.port,
181 ..Default::default()
182 })
183 .collect(),
184 ..Default::default()
185 })
186 .collect(),
187 error_message: Some(String::new()),
188 ..Default::default()
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use {
195 super::*,
196 crate::server::test_support::voter,
197 assert2::check,
198 krabka_protocol::owned::describe_quorum_request::{
199 PartitionData as RequestPartition, TopicData as RequestTopic,
200 },
201 std::collections::BTreeMap,
202 };
203
204 struct RequestedTopic<'a> {
206 name: &'a str,
207 partitions: &'a [i32],
208 }
209
210 type RequestedTopics<'a> = &'a [RequestedTopic<'a>];
212
213 fn request(topics: RequestedTopics<'_>) -> DescribeQuorumRequest {
214 DescribeQuorumRequest {
215 topics: topics
216 .iter()
217 .map(|topic| RequestTopic {
218 topic_name: topic.name.into(),
219 partitions: topic
220 .partitions
221 .iter()
222 .map(|&partition_index| RequestPartition {
223 partition_index,
224 ..Default::default()
225 })
226 .collect(),
227 ..Default::default()
228 })
229 .collect(),
230 ..Default::default()
231 }
232 }
233
234 fn endpoint(port: u16) -> krabka_metadata::VoterEndpoint {
235 krabka_metadata::VoterEndpoint {
236 name: "CONTROLLER".into(),
237 host: "controller".into(),
238 port,
239 }
240 }
241
242 fn leader_snapshot() -> QuorumStateSnapshot {
244 QuorumStateSnapshot {
245 leader_id: Some(NodeId(1)),
246 leader_epoch: 7,
247 high_watermark: 40,
248 last_applied_record_timestamp_ms: 700,
249 pending_record_processing_time_us_avg: 0,
250 pending_record_processing_time_us_max: 0,
251 quorum_high_watermark: 40,
252 log_end_offset: 42,
253 log_start_offset: 0,
254 voters: krabka_metadata::VoterSet::from_voters([
255 voter(1, vec![endpoint(9091)]),
256 voter(2, vec![endpoint(9092)]),
257 ]),
258 voted_directory_id: None,
259 observers: vec![ObserverReplica {
260 id: NodeId(9),
261 directory_id: uuid::Uuid::from_u128(99),
262 log_end_offset: 38,
263 last_fetch_ms: 800,
264 last_caught_up_ms: 700,
265 }],
266 per_replica_fetch_offset: BTreeMap::from([(NodeId(1), 42), (NodeId(2), 40)]),
267 per_replica_last_fetch_ms: BTreeMap::from([(NodeId(1), 1_000), (NodeId(2), 900)]),
268 per_replica_last_caught_up_ms: BTreeMap::from([(NodeId(1), 1_000), (NodeId(2), 850)]),
269 is_leader: true,
270 current_state: "leader",
271 }
272 }
273
274 fn replica(
275 id: i32,
276 directory: u128,
277 log_end_offset: i64,
278 fetch: i64,
279 caught_up: i64,
280 ) -> ReplicaState {
281 ReplicaState {
282 replica_id: id,
283 replica_directory_id: WireUuid(*uuid::Uuid::from_u128(directory).as_bytes()),
284 log_end_offset,
285 last_fetch_timestamp: fetch,
286 last_caught_up_timestamp: caught_up,
287 ..Default::default()
288 }
289 }
290
291 #[test]
292 fn the_leader_describes_every_replica() {
293 let response = describe_quorum(
294 &request(&[RequestedTopic {
295 name: METADATA_TOPIC,
296 partitions: &[0],
297 }]),
298 &leader_snapshot(),
299 );
300
301 check!(
302 response
303 == DescribeQuorumResponse {
304 topics: vec![TopicData {
305 topic_name: METADATA_TOPIC.into(),
306 partitions: vec![PartitionData {
307 partition_index: 0,
308 leader_id: 1,
309 leader_epoch: 7,
310 high_watermark: 40,
311 current_voters: vec![
312 replica(1, 1, 42, 1_000, 1_000),
313 replica(2, 2, 40, 900, 850),
314 ],
315 observers: vec![replica(9, 99, 38, 800, 700)],
316 error_message: Some(String::new()),
317 ..Default::default()
318 }],
319 ..Default::default()
320 }],
321 nodes: [(1, 9091), (2, 9092)]
322 .into_iter()
323 .map(|(node_id, port)| Node {
324 node_id,
325 listeners: vec![Listener {
326 name: "CONTROLLER".into(),
327 host: "controller".into(),
328 port,
329 ..Default::default()
330 }],
331 ..Default::default()
332 })
333 .collect(),
334 error_message: Some(String::new()),
335 ..Default::default()
336 }
337 );
338 }
339
340 #[test]
344 fn one_observer_id_under_two_directories_is_two_rows() {
345 let observer = |directory: u128, offset: i64, fetch: i64, caught_up: i64| ObserverReplica {
346 id: NodeId(9),
347 directory_id: uuid::Uuid::from_u128(directory),
348 log_end_offset: offset,
349 last_fetch_ms: fetch,
350 last_caught_up_ms: caught_up,
351 };
352 let snapshot = QuorumStateSnapshot {
353 observers: vec![observer(91, 30, 500, 400), observer(92, 10, 600, -1)],
354 ..leader_snapshot()
355 };
356
357 let response = describe_quorum(
358 &request(&[RequestedTopic {
359 name: METADATA_TOPIC,
360 partitions: &[0],
361 }]),
362 &snapshot,
363 );
364
365 check!(
366 response.topics[0].partitions[0].observers
367 == vec![replica(9, 91, 30, 500, 400), replica(9, 92, 10, 600, -1)]
368 );
369 }
370
371 #[test]
372 fn a_node_that_is_not_the_leader_refuses() {
373 let follower = QuorumStateSnapshot {
374 leader_id: Some(NodeId(2)),
375 is_leader: false,
376 current_state: "follower",
377 ..leader_snapshot()
378 };
379
380 check!(
381 describe_quorum(
382 &request(&[RequestedTopic {
383 name: METADATA_TOPIC,
384 partitions: &[0]
385 }]),
386 &follower
387 ) == not_leader_or_follower_response()
388 );
389 }
390
391 #[test]
395 fn an_unmappable_or_never_fetched_voter_gets_blank_fields() {
396 let huge_id = u64::from(u32::MAX) + 1;
397 let snapshot = QuorumStateSnapshot {
398 voters: krabka_metadata::VoterSet::from_voters([
399 voter(1, vec![endpoint(9091)]),
400 voter(huge_id, vec![endpoint(9093)]),
401 ]),
402 per_replica_fetch_offset: BTreeMap::from([(NodeId(1), 42)]),
403 per_replica_last_fetch_ms: BTreeMap::from([(NodeId(1), 1_000)]),
404 per_replica_last_caught_up_ms: BTreeMap::from([(NodeId(1), 1_000)]),
405 observers: vec![],
406 ..leader_snapshot()
407 };
408
409 let response = describe_quorum(
410 &request(&[RequestedTopic {
411 name: METADATA_TOPIC,
412 partitions: &[0],
413 }]),
414 &snapshot,
415 );
416 let partition = &response.topics[0].partitions[0];
417 let unmapped = partition
418 .current_voters
419 .iter()
420 .find(|voter| voter.replica_id == -1)
421 .expect("the oversized id maps to -1");
422 check!(unmapped.log_end_offset == -1, "never fetched");
423 check!(unmapped.last_fetch_timestamp == -1);
424 check!(unmapped.last_caught_up_timestamp == -1);
425 check!(
426 response.nodes.iter().any(|node| node.node_id == -1),
427 "the same id maps to -1 in Nodes"
428 );
429 }
430
431 #[test]
435 fn a_request_for_another_partition_gets_unknown_topic_or_partition() {
436 struct Row {
437 label: &'static str,
438 topics: RequestedTopics<'static>,
439 }
440 let rows = [
441 Row {
442 label: "another topic",
443 topics: &[RequestedTopic {
444 name: "other",
445 partitions: &[0],
446 }],
447 },
448 Row {
449 label: "partition 1",
450 topics: &[RequestedTopic {
451 name: METADATA_TOPIC,
452 partitions: &[1],
453 }],
454 },
455 Row {
456 label: "two partitions",
457 topics: &[RequestedTopic {
458 name: METADATA_TOPIC,
459 partitions: &[0, 1],
460 }],
461 },
462 Row {
463 label: "two topics",
464 topics: &[
465 RequestedTopic {
466 name: METADATA_TOPIC,
467 partitions: &[0],
468 },
469 RequestedTopic {
470 name: "other",
471 partitions: &[0],
472 },
473 ],
474 },
475 Row {
476 label: "no topic",
477 topics: &[],
478 },
479 ];
480 let expected_partition = |partition_index| PartitionData {
482 partition_index,
483 error_code: 3,
484 error_message: Some(UNKNOWN_TOPIC_OR_PARTITION_MESSAGE.into()),
485 ..Default::default()
486 };
487 for Row { label, topics } in rows {
488 let expected = DescribeQuorumResponse {
489 topics: topics
490 .iter()
491 .map(|topic| TopicData {
492 topic_name: topic.name.into(),
493 partitions: topic
494 .partitions
495 .iter()
496 .map(|&index| expected_partition(index))
497 .collect(),
498 ..Default::default()
499 })
500 .collect(),
501 error_message: Some(String::new()),
502 ..Default::default()
503 };
504 check!(
505 describe_quorum(&request(topics), &leader_snapshot()) == expected,
506 "{label}"
507 );
508 }
509 }
510}