Skip to main content

krabka_client_admin/
elections.rs

1//! Leader elections (`ElectLeaders`, KIP-183 and KIP-460), as Kafka's
2//! `Admin.electLeaders`.
3
4use std::collections::BTreeMap;
5
6use krabka_protocol::owned::{
7    elect_leaders_request::{ElectLeadersRequest, TopicPartitions},
8    elect_leaders_response::ElectLeadersResponse,
9};
10
11use crate::{
12    AdminClient, AdminError, KafkaError, NOT_CONTROLLER, kafka_error_name, retry::ControllerRetry,
13};
14
15/// Which replica an election makes the leader, as Kafka's `ElectionType`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ElectionType {
18    /// The preferred replica, the first of the assignment, when it is in
19    /// sync (KIP-183).
20    Preferred,
21    /// Any live replica, even one that is out of sync, when no in-sync
22    /// replica is live (KIP-460). Needs `ElectLeaders` v1.
23    Unclean,
24}
25
26impl ElectionType {
27    const fn wire(self) -> i8 {
28        match self {
29            Self::Preferred => 0,
30            Self::Unclean => 1,
31        }
32    }
33}
34
35/// The election result of each `(topic, partition)`, as Kafka's
36/// `ElectLeadersResult.partitions`: `Ok` for an elected leader, or the
37/// partition's error.
38pub type ElectionResults = BTreeMap<(String, i32), Result<(), KafkaError>>;
39
40/// The `ElectLeaders` request of `partitions`, grouped by topic, as Kafka's
41/// `ElectLeadersRequest.Builder` builds it. `None` asks for every partition
42/// of the cluster (a null `topic_partitions`).
43fn elect_leaders_request(
44    election_type: ElectionType,
45    partitions: Option<&[(String, i32)]>,
46    timeout_ms: i32,
47) -> ElectLeadersRequest {
48    let topic_partitions = partitions.map(|partitions| {
49        let mut by_topic = BTreeMap::<&str, Vec<i32>>::new();
50        for (topic, partition) in partitions {
51            let entry = by_topic.entry(topic.as_str()).or_default();
52            if !entry.contains(partition) {
53                entry.push(*partition);
54            }
55        }
56        by_topic
57            .into_iter()
58            .map(|(topic, partitions)| TopicPartitions {
59                topic: topic.to_owned(),
60                partitions,
61                ..Default::default()
62            })
63            .collect()
64    });
65    ElectLeadersRequest {
66        election_type: election_type.wire(),
67        topic_partitions,
68        timeout_ms,
69        ..Default::default()
70    }
71}
72
73/// Whether Kafka's `handleNotControllerError` finds the controller again
74/// after this answer: its top-level code or a partition code is
75/// `NOT_CONTROLLER`.
76fn names_not_controller(response: &ElectLeadersResponse) -> bool {
77    response.error_code == NOT_CONTROLLER
78        || response
79            .replica_election_results
80            .iter()
81            .flat_map(|topic| &topic.partition_result)
82            .any(|partition| partition.error_code == NOT_CONTROLLER)
83}
84
85/// Maps an `ElectLeaders` answer to the result of each partition, as Kafka's
86/// `ElectLeadersResponse.electLeadersResult` does. A top-level error fails
87/// the whole call.
88fn elect_leaders_results(response: ElectLeadersResponse) -> Result<ElectionResults, AdminError> {
89    if response.error_code != 0 {
90        return Err(AdminError::Broker {
91            api: "ElectLeaders",
92            code: response.error_code,
93            name: kafka_error_name(response.error_code),
94            message: None,
95        });
96    }
97    Ok(response
98        .replica_election_results
99        .into_iter()
100        .flat_map(|topic| {
101            let name = topic.topic;
102            topic.partition_result.into_iter().map(move |partition| {
103                let result = if partition.error_code == 0 {
104                    Ok(())
105                } else {
106                    Err(KafkaError {
107                        code: partition.error_code,
108                        name: kafka_error_name(partition.error_code),
109                        message: partition.error_message,
110                    })
111                };
112                ((name.clone(), partition.partition_id), result)
113            })
114        })
115        .collect())
116}
117
118impl AdminClient {
119    /// Elects a leader for each partition of `partitions`, or of every
120    /// partition of the cluster for `None`, as Kafka's `electLeaders`
121    /// operation does.
122    ///
123    /// The client sends `ElectLeaders` to the controller connection, as
124    /// Kafka's `KafkaAdminClient` sends it with a `ControllerNodeProvider`,
125    /// with the time left before the call deadline as its `timeout_ms`.
126    /// [`ElectionType::Unclean`] needs `ElectLeaders` v1, so the client
127    /// negotiates v1 or higher for it, as Kafka's builder refuses v0.
128    ///
129    /// `NOT_CONTROLLER` (41), top-level or on any partition, makes the
130    /// client find the controller again and resend the request with the
131    /// backoff, until Kafka's default `default.api.timeout.ms` (60 s).
132    ///
133    /// The result has an entry for each partition that the controller
134    /// answered for. A partition that needed no election gets
135    /// `ELECTION_NOT_NEEDED` (84), as Kafka gives an
136    /// `ElectionNotNeededException`.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`AdminError::Broker`] for a top-level error, such as
141    /// `CLUSTER_AUTHORIZATION_FAILED` (31), and with `REQUEST_TIMED_OUT` (7)
142    /// at the deadline. Returns [`AdminError::Transport`] when the
143    /// connection fails, or when the controller does not support the
144    /// `ElectLeaders` version that `election_type` needs.
145    pub async fn elect_leaders(
146        &mut self,
147        election_type: ElectionType,
148        partitions: Option<&[(String, i32)]>,
149    ) -> Result<ElectionResults, AdminError> {
150        let mut retry = ControllerRetry::new("ElectLeaders", self.retry);
151        loop {
152            let request =
153                elect_leaders_request(election_type, partitions, retry.remaining_millis());
154            let response = match election_type {
155                ElectionType::Preferred => retry.bounded(self.conn.send(request)).await?,
156                ElectionType::Unclean => retry.bounded(self.conn.send_at_least(request, 1)).await?,
157            };
158            if !names_not_controller(&response) {
159                return elect_leaders_results(response);
160            }
161            retry.after_not_controller(self).await?;
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use std::{
169        net::SocketAddr,
170        sync::{
171            Arc, Mutex,
172            atomic::{AtomicU16, Ordering},
173        },
174    };
175
176    use krabka_client_core::MockBroker;
177    use krabka_protocol::owned::{
178        api_versions_request, elect_leaders_request,
179        elect_leaders_response::{PartitionResult, ReplicaElectionResult},
180        metadata_request,
181    };
182
183    use super::*;
184    use crate::partition_leaders::test_support::{
185        ApiVersionRange, api_versions, decode_request, encode_response, fast_admin, orders_metadata,
186    };
187
188    fn orders(partitions: &[i32]) -> Vec<(String, i32)> {
189        partitions
190            .iter()
191            .map(|partition| ("orders".to_owned(), *partition))
192            .collect()
193    }
194
195    #[test]
196    fn request_groups_partitions_by_topic_or_asks_for_all() {
197        let keys = vec![
198            ("orders".to_owned(), 1),
199            ("audit".to_owned(), 0),
200            ("orders".to_owned(), 0),
201            ("orders".to_owned(), 1),
202        ];
203        for (name, election_type, partitions, expected) in [
204            (
205                "preferred for listed partitions",
206                ElectionType::Preferred,
207                Some(keys.as_slice()),
208                ElectLeadersRequest {
209                    election_type: 0,
210                    topic_partitions: Some(vec![
211                        TopicPartitions {
212                            topic: "audit".to_owned(),
213                            partitions: vec![0],
214                            ..Default::default()
215                        },
216                        TopicPartitions {
217                            topic: "orders".to_owned(),
218                            partitions: vec![1, 0],
219                            ..Default::default()
220                        },
221                    ]),
222                    timeout_ms: 30_000,
223                    ..Default::default()
224                },
225            ),
226            (
227                "unclean for every partition",
228                ElectionType::Unclean,
229                None,
230                ElectLeadersRequest {
231                    election_type: 1,
232                    topic_partitions: None,
233                    timeout_ms: 30_000,
234                    ..Default::default()
235                },
236            ),
237        ] {
238            assert2::assert!(
239                elect_leaders_request(election_type, partitions, 30_000) == expected,
240                "case {name}"
241            );
242        }
243    }
244
245    /// An `ElectLeaders` answer with `top_code` and `code` on `orders`-0.
246    fn election_answer(top_code: i16, code: i16) -> ElectLeadersResponse {
247        ElectLeadersResponse {
248            error_code: top_code,
249            replica_election_results: vec![ReplicaElectionResult {
250                topic: "orders".to_owned(),
251                partition_result: vec![PartitionResult {
252                    partition_id: 0,
253                    error_code: code,
254                    error_message: (code != 0).then(|| "no election".to_owned()),
255                    ..Default::default()
256                }],
257                ..Default::default()
258            }],
259            ..Default::default()
260        }
261    }
262
263    /// Kafka's `electLeaders` maps each partition code to its own result,
264    /// fails the call on a top-level code, and finds the controller again
265    /// after `NOT_CONTROLLER`.
266    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
267    async fn elect_leaders_retries_not_controller_and_maps_each_partition() {
268        let not_needed = Err(KafkaError {
269            code: 84,
270            name: "ELECTION_NOT_NEEDED",
271            message: Some("no election".to_owned()),
272        });
273        for (name, election_type, answers, expected) in [
274            (
275                "success",
276                ElectionType::Preferred,
277                vec![(0, 0)],
278                (
279                    Ok(maplit::btreemap! {
280                        ("orders".to_owned(), 0) => Ok(()),
281                    }),
282                    1,
283                ),
284            ),
285            (
286                "election not needed is a partition result",
287                ElectionType::Unclean,
288                vec![(0, 84)],
289                (
290                    Ok(maplit::btreemap! {
291                        ("orders".to_owned(), 0) => not_needed,
292                    }),
293                    1,
294                ),
295            ),
296            (
297                "not controller finds the controller again",
298                ElectionType::Preferred,
299                vec![(41, 0), (0, NOT_CONTROLLER), (0, 0)],
300                (
301                    Ok(maplit::btreemap! {
302                        ("orders".to_owned(), 0) => Ok(()),
303                    }),
304                    3,
305                ),
306            ),
307            (
308                "cluster authorization failed is final",
309                ElectionType::Preferred,
310                vec![(31, 0)],
311                (Err(("ElectLeaders", 31)), 1),
312            ),
313        ] {
314            let requests = Arc::new(Mutex::new(Vec::<(i16, ElectLeadersRequest)>::new()));
315            let handler_requests = Arc::clone(&requests);
316            let port = Arc::new(AtomicU16::new(0));
317            let handler_port = Arc::clone(&port);
318            let broker = MockBroker::start(move |api_key, version, _, body| {
319                let own = SocketAddr::from(([127, 0, 0, 1], handler_port.load(Ordering::SeqCst)));
320                match api_key {
321                    api_versions_request::API_KEY => Some(
322                        api_versions()
323                            .apis(vec![
324                                ApiVersionRange::builder()
325                                    .api_key(elect_leaders_request::API_KEY)
326                                    .max_version(2)
327                                    .build(),
328                            ])
329                            .call(),
330                    ),
331                    metadata_request::API_KEY => {
332                        Some(encode_response(&orders_metadata(own), version, true))
333                    }
334                    elect_leaders_request::API_KEY => {
335                        let flexible = version >= elect_leaders_request::FLEXIBLE_MIN;
336                        let mut requests = handler_requests.lock().expect("requests lock");
337                        let (top, code) = answers[requests.len().min(answers.len() - 1)];
338                        requests.push((version, decode_request(body, version, flexible)));
339                        drop(requests);
340                        Some(encode_response(
341                            &election_answer(top, code),
342                            version,
343                            flexible,
344                        ))
345                    }
346                    _ => None,
347                }
348            })
349            .await
350            .unwrap();
351            port.store(broker.addr.port(), Ordering::SeqCst);
352            let mut admin = fast_admin(broker.addr, krabka_units::secs(5)).await;
353
354            let result = admin
355                .elect_leaders(election_type, Some(&orders(&[0])))
356                .await
357                .map_err(|error| match error {
358                    AdminError::Broker { api, code, .. } => (api, code),
359                    other => panic!("case {name}: unexpected error {other:?}"),
360                });
361
362            broker.stop();
363            let requests = requests.lock().expect("requests lock").clone();
364            let shapes = requests
365                .iter()
366                .map(|(version, request)| {
367                    (
368                        *version,
369                        ElectLeadersRequest {
370                            timeout_ms: 0,
371                            ..request.clone()
372                        },
373                    )
374                })
375                .collect::<Vec<_>>();
376            let expected_shape = (
377                2,
378                elect_leaders_request(election_type, Some(&orders(&[0])), 0),
379            );
380            assert2::assert!(
381                (result, shapes) == (expected.0, vec![expected_shape; expected.1]),
382                "case {name}"
383            );
384            assert2::assert!(
385                requests
386                    .iter()
387                    .all(|(_, request)| (1..=5_000).contains(&request.timeout_ms)),
388                "case {name}: the timeout is the time left of the call"
389            );
390        }
391    }
392
393    /// Kafka's `ElectLeadersRequest.Builder` refuses an unclean election
394    /// below v1, so a controller with v0 only fails the call before it sends
395    /// anything.
396    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
397    async fn unclean_election_needs_elect_leaders_v1() {
398        let requests = Arc::new(Mutex::new(0_usize));
399        let handler_requests = Arc::clone(&requests);
400        let broker = MockBroker::start(move |api_key, version, _, _| match api_key {
401            api_versions_request::API_KEY => Some(
402                api_versions()
403                    .apis(vec![
404                        ApiVersionRange::builder()
405                            .api_key(elect_leaders_request::API_KEY)
406                            .build(),
407                    ])
408                    .call(),
409            ),
410            elect_leaders_request::API_KEY => {
411                *handler_requests.lock().expect("requests lock") += 1;
412                Some(encode_response(&election_answer(0, 0), version, false))
413            }
414            _ => None,
415        })
416        .await
417        .unwrap();
418        let mut admin = fast_admin(broker.addr, krabka_units::secs(5)).await;
419
420        let result = admin
421            .elect_leaders(ElectionType::Unclean, Some(&orders(&[0])))
422            .await;
423
424        broker.stop();
425        assert2::assert!(matches!(
426            result,
427            Err(AdminError::Transport(
428                krabka_client_core::ClientError::IncompatibleVersion { .. }
429            ))
430        ));
431        assert2::assert!(*requests.lock().expect("requests lock") == 0);
432    }
433}