Skip to main content

krabka_client_admin/
log_dirs.rs

1//! KIP-113 admin RPCs: `AlterReplicaLogDirs` (`api_key` 34) and
2//! `DescribeLogDirs` (`api_key` 35).
3//!
4//! Both act only on the broker that receives the request. As Kafka's
5//! `KafkaAdminClient.describeLogDirs` and `alterReplicaLogDirs` do, the admin
6//! client finds each broker by id in the metadata and sends one request to
7//! each broker (`ConstantNodeIdProvider`). A broker that fails gives an error
8//! for that broker only.
9
10use std::{
11    collections::{BTreeMap, BTreeSet},
12    mem,
13};
14
15use futures_util::{StreamExt as _, future::join_all, stream::FuturesUnordered};
16use krabka_client_core::{Connection, ConnectionOptions};
17use krabka_protocol::{
18    ProtocolRequest,
19    owned::{
20        alter_replica_log_dirs_request::{
21            AlterReplicaLogDir, AlterReplicaLogDirTopic, AlterReplicaLogDirsRequest,
22        },
23        alter_replica_log_dirs_response::AlterReplicaLogDirsResponse,
24        describe_cluster_request::DescribeClusterRequest,
25        describe_log_dirs_request::{DescribableLogDirTopic, DescribeLogDirsRequest},
26        describe_log_dirs_response::DescribeLogDirsResponse,
27        metadata_request::MetadataRequest,
28    },
29};
30use krabka_units::{ByteSize, convert::ByteSizeExt as _};
31
32use crate::{
33    AdminClient, AdminError, KafkaError, RecoveringConnection, format_host_port,
34    groups::list_groups_kafka_error,
35    kafka_error_if, kafka_error_name,
36    retry::{CoordinatorRetry, RetryAction, RetryPolicy, is_connection_failure},
37    send_connection_at_least,
38};
39
40/// `UNKNOWN_SERVER_ERROR`.
41const UNKNOWN_SERVER_ERROR: i16 = -1;
42/// `REQUEST_TIMED_OUT`: Kafka's `TimeoutException`.
43const REQUEST_TIMED_OUT: i16 = 7;
44/// `CLUSTER_AUTHORIZATION_FAILED`.
45const CLUSTER_AUTHORIZATION_FAILED: i16 = 31;
46/// `KAFKA_STORAGE_ERROR`: the log dir is offline, or the broker does not
47/// host the replica.
48const KAFKA_STORAGE_ERROR: i16 = 56;
49/// `DescribeCluster` `EndpointType.CONTROLLER` (KIP-919).
50const ENDPOINT_TYPE_CONTROLLERS: i8 = 2;
51
52/// One replica of a partition on one broker, as Kafka's
53/// `TopicPartitionReplica`.
54#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
55pub struct TopicPartitionReplica {
56    pub topic: String,
57    pub partition: i32,
58    pub broker_id: i32,
59}
60
61/// One log dir from a `DescribeLogDirs` response, as Kafka's
62/// `LogDirDescription`.
63#[derive(Debug, Clone, PartialEq)]
64pub struct LogDirInfo {
65    pub log_dir: String,
66    pub error: Option<KafkaError>,
67    pub topics: Vec<LogDirTopicInfo>,
68    /// The size of the volume. `None` when the broker does not report it
69    /// (`DescribeLogDirs` below v4, or `-1`).
70    pub total: Option<ByteSize>,
71    /// The free space of the volume that the log dir can use. `None` when the
72    /// broker does not report it.
73    pub usable: Option<ByteSize>,
74    /// Whether the log dir takes no new partitions (KIP-1066).
75    pub is_cordoned: bool,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct LogDirTopicInfo {
80    pub name: String,
81    pub partitions: Vec<LogDirPartitionInfo>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct LogDirPartitionInfo {
86    pub partition_index: i32,
87    pub partition_size: i64,
88    pub offset_lag: i64,
89    pub is_future_key: bool,
90}
91
92/// The result of one broker: its value, or the Kafka error of the broker.
93pub type BrokerResult<T> = Result<T, KafkaError>;
94
95/// Where one replica of a partition lives on its broker.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ReplicaLogDir {
98    /// The absolute path of the log dir.
99    pub path: String,
100    /// How far the replica lags behind the log end offset of the partition
101    /// on this broker (for the current replica) or behind the current
102    /// replica (for a future replica).
103    pub offset_lag: i64,
104}
105
106/// The log dirs of one replica, as Kafka's
107/// `DescribeReplicaLogDirsResult.ReplicaLogDirInfo`. A replica that the
108/// broker does not hold, or holds only in an offline log dir, has neither.
109#[derive(Debug, Clone, Default, PartialEq, Eq)]
110pub struct ReplicaLogDirInfo {
111    /// The log dir of the current replica.
112    pub current: Option<ReplicaLogDir>,
113    /// The log dir that an `AlterReplicaLogDirs` move is copying the replica
114    /// to (KIP-113).
115    pub future: Option<ReplicaLogDir>,
116}
117
118impl AdminClient {
119    /// `AlterReplicaLogDirs` (KIP-113): moves replicas between the
120    /// `log.dirs` of their brokers, as Kafka's
121    /// `KafkaAdminClient.alterReplicaLogDirs` does.
122    ///
123    /// `assignments` maps each replica to the absolute path of its target log
124    /// dir. The call groups the replicas by broker id and sends one request to
125    /// each broker, all at the same time. The result has one entry for each
126    /// replica:
127    ///
128    /// - The error code of the broker for that partition, `Ok(())` for 0.
129    /// - `UNKNOWN_SERVER_ERROR` (-1) when the response of the broker has no
130    ///   result for the replica (`completeUnrealizedFutures`).
131    /// - The error of the broker call for every replica of a broker that
132    ///   fails. The call finds each broker by id in fresh metadata for each
133    ///   connection attempt, so a broker that is missing from the metadata or
134    ///   that moved to a new address is found again. A missing broker and a
135    ///   failed or lost connection are tried again with Kafka's backoff until
136    ///   `default.api.timeout.ms` (60 s), and then give `REQUEST_TIMED_OUT`
137    ///   (7). A slow or missing broker does not delay the other brokers.
138    pub async fn alter_replica_log_dirs(
139        &mut self,
140        assignments: &BTreeMap<TopicPartitionReplica, String>,
141    ) -> BTreeMap<TopicPartitionReplica, BrokerResult<()>> {
142        self.alter_replica_log_dirs_with_retry(assignments, self.retry)
143            .await
144    }
145
146    async fn alter_replica_log_dirs_with_retry(
147        &self,
148        assignments: &BTreeMap<TopicPartitionReplica, String>,
149        retry: RetryPolicy,
150    ) -> BTreeMap<TopicPartitionReplica, BrokerResult<()>> {
151        let start = retry.start();
152        let mut by_broker = BTreeMap::<i32, BTreeMap<TopicPartitionReplica, String>>::new();
153        for (replica, path) in assignments {
154            by_broker
155                .entry(replica.broker_id)
156                .or_default()
157                .insert(replica.clone(), path.clone());
158        }
159        let (conn, options) = (&self.conn, &self.options);
160        let answers = join_all(by_broker.iter().map(|(broker_id, moves)| {
161            call_broker(
162                conn,
163                *broker_id,
164                options,
165                alter_request(moves),
166                CoordinatorRetry::from_deadline(start),
167            )
168        }))
169        .await;
170
171        let mut out = BTreeMap::new();
172        for ((broker_id, moves), answer) in by_broker.into_iter().zip(answers) {
173            match answer {
174                Ok(response) => out.extend(alter_results(broker_id, &moves, response)),
175                Err(error) => {
176                    out.extend(
177                        moves
178                            .into_keys()
179                            .map(|replica| (replica, Err(error.clone()))),
180                    );
181                }
182            }
183        }
184        out
185    }
186
187    /// `DescribeLogDirs` (KIP-113): lists every configured `log.dir` of each
188    /// broker in `brokers`, with the partitions each one holds, as Kafka's
189    /// `KafkaAdminClient.describeLogDirs` does.
190    ///
191    /// Pass `None` to fetch all partitions, as Kafka does. Pass `Some` with a
192    /// topic to partitions filter to narrow the result. An empty inner vec
193    /// means all partitions of that topic.
194    ///
195    /// The result has one entry for each broker id, and the brokers are asked
196    /// at the same time. A broker that answers with no log dir gives its
197    /// top-level error code, or `CLUSTER_AUTHORIZATION_FAILED` (31) when it
198    /// has none, as Kafka does. The call finds each broker by id in fresh
199    /// metadata for each connection attempt. A missing broker and a failed or
200    /// lost connection are tried again with Kafka's backoff until
201    /// `default.api.timeout.ms` (60 s), and then give `REQUEST_TIMED_OUT` (7).
202    pub async fn describe_log_dirs(
203        &mut self,
204        brokers: &[i32],
205        filter: Option<&BTreeMap<String, Vec<i32>>>,
206    ) -> BTreeMap<i32, BrokerResult<Vec<LogDirInfo>>> {
207        self.describe_log_dirs_with_retry(brokers, filter, self.retry)
208            .await
209    }
210
211    async fn describe_log_dirs_with_retry(
212        &self,
213        brokers: &[i32],
214        filter: Option<&BTreeMap<String, Vec<i32>>>,
215        retry: RetryPolicy,
216    ) -> BTreeMap<i32, BrokerResult<Vec<LogDirInfo>>> {
217        let start = retry.start();
218        let broker_ids = brokers.iter().copied().collect::<BTreeSet<_>>();
219        let request = describe_request(filter);
220        let (conn, options) = (&self.conn, &self.options);
221        let answers = join_all(broker_ids.iter().map(|broker_id| {
222            let request = request.clone();
223            async move {
224                let response = call_broker(
225                    conn,
226                    *broker_id,
227                    options,
228                    request,
229                    CoordinatorRetry::from_deadline(start),
230                )
231                .await?;
232                log_dir_infos(response)
233            }
234        }))
235        .await;
236        broker_ids.into_iter().zip(answers).collect()
237    }
238
239    /// `DescribeLogDirs` for single replicas: finds the log dir of each
240    /// replica of `replicas`, and of its future replica when one is being
241    /// moved, as Kafka's `KafkaAdminClient.describeReplicaLogDirs` does.
242    ///
243    /// The call groups the replicas by broker id and sends each broker one
244    /// `DescribeLogDirs` request that names only its partitions, all at the
245    /// same time. Each broker is found and retried as in
246    /// [`Self::describe_log_dirs`]. The result has one entry for each
247    /// replica, and the brokers complete their replicas in the order they
248    /// answer, as Kafka completes the replica futures:
249    ///
250    /// - The log dirs that the broker reports for the partition. A log dir
251    ///   with `KAFKA_STORAGE_ERROR` (56) is offline and skipped, as Kafka
252    ///   does, so a replica that lives only there has neither log dir. A
253    ///   replica that the answer does not name has neither log dir.
254    /// - The top-level error code of the answer is not read, as Kafka's
255    ///   `describeReplicaLogDirs` does not read it: a `DescribeLogDirs` v3+
256    ///   answer with `CLUSTER_AUTHORIZATION_FAILED` (31) and no log dir gives
257    ///   each replica of that broker neither log dir.
258    /// - A log dir with any other error fails the call, as Kafka's
259    ///   `handleFailure` with an `IllegalStateException` completes every
260    ///   replica future of the call that is still pending: the replicas of
261    ///   that broker and of every broker that has not answered yet get that
262    ///   error. The replicas of brokers that answered before keep their log
263    ///   dirs.
264    /// - A broker call that fails, with `REQUEST_TIMED_OUT` (7) at
265    ///   `default.api.timeout.ms` or another error, fails every pending
266    ///   replica of the call in the same way.
267    ///
268    /// The call returns once no replica is pending; it does not wait for the
269    /// brokers that have not answered after a failure.
270    pub async fn describe_replica_log_dirs(
271        &mut self,
272        replicas: &[TopicPartitionReplica],
273    ) -> BTreeMap<TopicPartitionReplica, BrokerResult<ReplicaLogDirInfo>> {
274        self.describe_replica_log_dirs_with_retry(replicas, self.retry)
275            .await
276    }
277
278    async fn describe_replica_log_dirs_with_retry(
279        &self,
280        replicas: &[TopicPartitionReplica],
281        retry: RetryPolicy,
282    ) -> BTreeMap<TopicPartitionReplica, BrokerResult<ReplicaLogDirInfo>> {
283        let start = retry.start();
284        let mut by_broker = BTreeMap::<i32, BTreeMap<String, BTreeSet<i32>>>::new();
285        for replica in replicas {
286            by_broker
287                .entry(replica.broker_id)
288                .or_default()
289                .entry(replica.topic.clone())
290                .or_default()
291                .insert(replica.partition);
292        }
293        let (conn, options) = (&self.conn, &self.options);
294        let mut results = ReplicaResults::new(&by_broker);
295        let mut answers = by_broker
296            .iter()
297            .map(|(broker_id, topics)| async move {
298                let answer = call_broker(
299                    conn,
300                    *broker_id,
301                    options,
302                    replica_request(topics),
303                    CoordinatorRetry::from_deadline(start),
304                )
305                .await;
306                (
307                    *broker_id,
308                    topics,
309                    answer.and_then(|response| replica_log_dirs(*broker_id, topics, response)),
310                )
311            })
312            .collect::<FuturesUnordered<_>>();
313        while !results.is_complete()
314            && let Some((broker_id, topics, answer)) = answers.next().await
315        {
316            results.complete(broker_id, topics, answer);
317        }
318        results.finish()
319    }
320}
321
322/// The result of each replica of a `describe_replica_log_dirs` call, as
323/// Kafka's replica futures: a broker's answer completes the replicas of that
324/// broker that are still pending, and a failure completes every replica of
325/// the call that is still pending (`completeAllExceptionally`).
326#[derive(Debug)]
327struct ReplicaResults {
328    pending: BTreeSet<TopicPartitionReplica>,
329    done: BTreeMap<TopicPartitionReplica, BrokerResult<ReplicaLogDirInfo>>,
330}
331
332impl ReplicaResults {
333    fn new(by_broker: &BTreeMap<i32, BTreeMap<String, BTreeSet<i32>>>) -> Self {
334        let pending = by_broker
335            .iter()
336            .flat_map(|(broker_id, topics)| {
337                topics.iter().flat_map(move |(topic, partitions)| {
338                    partitions
339                        .iter()
340                        .map(move |partition| TopicPartitionReplica {
341                            topic: topic.clone(),
342                            partition: *partition,
343                            broker_id: *broker_id,
344                        })
345                })
346            })
347            .collect();
348        Self {
349            pending,
350            done: BTreeMap::new(),
351        }
352    }
353
354    /// Complete the replicas with the answer of broker `broker_id`, which the
355    /// request asked about `topics`.
356    fn complete(
357        &mut self,
358        broker_id: i32,
359        topics: &BTreeMap<String, BTreeSet<i32>>,
360        answer: BrokerResult<BTreeMap<(String, i32), ReplicaLogDirInfo>>,
361    ) {
362        match answer {
363            Ok(mut infos) => {
364                for (topic, partitions) in topics {
365                    for partition in partitions {
366                        let replica = TopicPartitionReplica {
367                            topic: topic.clone(),
368                            partition: *partition,
369                            broker_id,
370                        };
371                        if self.pending.remove(&replica) {
372                            let info = infos
373                                .remove(&(topic.clone(), *partition))
374                                .unwrap_or_default();
375                            self.done.insert(replica, Ok(info));
376                        }
377                    }
378                }
379            }
380            Err(error) => {
381                for replica in mem::take(&mut self.pending) {
382                    self.done.insert(replica, Err(error.clone()));
383                }
384            }
385        }
386    }
387
388    fn is_complete(&self) -> bool {
389        self.pending.is_empty()
390    }
391
392    fn finish(self) -> BTreeMap<TopicPartitionReplica, BrokerResult<ReplicaLogDirInfo>> {
393        self.done
394    }
395}
396
397/// The `DescribeLogDirs` request of one broker for the partitions of
398/// `topics`, as Kafka's `describeReplicaLogDirs` builds it.
399fn replica_request(topics: &BTreeMap<String, BTreeSet<i32>>) -> DescribeLogDirsRequest {
400    DescribeLogDirsRequest {
401        topics: Some(
402            topics
403                .iter()
404                .map(|(topic, partitions)| DescribableLogDirTopic {
405                    topic: topic.clone(),
406                    partitions: partitions.iter().copied().collect(),
407                    ..Default::default()
408                })
409                .collect(),
410        ),
411        ..Default::default()
412    }
413}
414
415/// The log dirs of each asked-for partition in one broker's answer, as
416/// Kafka's `describeReplicaLogDirs` `handleResponse` reads them. The
417/// top-level error code is not read, as Kafka does not read it. A partition
418/// that the request did not name is skipped with a warning.
419fn replica_log_dirs(
420    broker_id: i32,
421    topics: &BTreeMap<String, BTreeSet<i32>>,
422    response: DescribeLogDirsResponse,
423) -> BrokerResult<BTreeMap<(String, i32), ReplicaLogDirInfo>> {
424    let mut out = BTreeMap::<(String, i32), ReplicaLogDirInfo>::new();
425    for dir in response.results {
426        if dir.error_code == KAFKA_STORAGE_ERROR {
427            continue;
428        }
429        if dir.error_code != 0 {
430            return Err(KafkaError {
431                code: dir.error_code,
432                name: kafka_error_name(dir.error_code),
433                message: Some(format!(
434                    "the error {} for log directory {} in the response from broker {broker_id} \
435                     is illegal",
436                    kafka_error_name(dir.error_code),
437                    dir.log_dir
438                )),
439            });
440        }
441        for topic in dir.topics {
442            for partition in topic.partitions {
443                let asked = topics
444                    .get(&topic.name)
445                    .is_some_and(|partitions| partitions.contains(&partition.partition_index));
446                if !asked {
447                    tracing::warn!(
448                        broker_id,
449                        topic = %topic.name,
450                        partition = partition.partition_index,
451                        "the DescribeLogDirs response names a partition that is not in the request"
452                    );
453                    continue;
454                }
455                let info = out
456                    .entry((topic.name.clone(), partition.partition_index))
457                    .or_default();
458                let location = Some(ReplicaLogDir {
459                    path: dir.log_dir.clone(),
460                    offset_lag: partition.offset_lag,
461                });
462                if partition.is_future_key {
463                    info.future = location;
464                } else {
465                    info.current = location;
466                }
467            }
468        }
469    }
470    Ok(out)
471}
472
473/// Send `request` to broker `broker_id` until it answers, the error is
474/// final, or the call deadline passes, as one Kafka `Call` with a
475/// `ConstantNodeIdProvider` does.
476///
477/// Each connection attempt looks the broker up in fresh metadata. A broker
478/// that the metadata does not name, and a failed or lost connection
479/// (including a TLS or SASL handshake with no verdict), are tried again after
480/// the backoff, as Kafka's `Call.fail` retries a `RetriableException`.
481async fn call_broker<R>(
482    conn: &RecoveringConnection,
483    broker_id: i32,
484    options: &ConnectionOptions,
485    request: R,
486    retry: CoordinatorRetry,
487) -> BrokerResult<R::Response>
488where
489    R: ProtocolRequest + Clone,
490{
491    call_node(conn, NodeTarget::broker(broker_id), options, request, retry).await
492}
493
494/// The node of a [`call_node`] call, as Kafka's `ConstantNodeIdProvider`.
495#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496pub(crate) struct NodeTarget {
497    /// The id of the node.
498    pub(crate) node_id: i32,
499    /// Whether the call finds the node among the controllers when the client
500    /// bootstraps from controllers (KIP-919), as a provider with
501    /// `supportsUseControllers` does. A call without it looks the node up in
502    /// `Metadata`.
503    pub(crate) supports_controllers: bool,
504    /// The lowest version of the request that the call may send.
505    pub(crate) min_version: Option<i16>,
506}
507
508impl NodeTarget {
509    /// A broker, found in `Metadata`, at any version.
510    const fn broker(node_id: i32) -> Self {
511        Self {
512            node_id,
513            supports_controllers: false,
514            min_version: None,
515        }
516    }
517}
518
519/// Send `request` to the node of `target`, as [`call_broker`] does for a
520/// broker.
521pub(crate) async fn call_node<R>(
522    conn: &RecoveringConnection,
523    target: NodeTarget,
524    options: &ConnectionOptions,
525    request: R,
526    mut retry: CoordinatorRetry,
527) -> BrokerResult<R::Response>
528where
529    R: ProtocolRequest + Clone,
530{
531    let mut connection: Option<Connection> = None;
532    loop {
533        let action = retry
534            .run(broker_attempt(
535                &mut connection,
536                conn,
537                target,
538                options,
539                request.clone(),
540            ))
541            .await;
542        if let Some(result) = retry.next(action).await {
543            return result.map_err(|error| broker_error(target.node_id, &error));
544        }
545    }
546}
547
548/// The Kafka error of a failed broker call. A timeout is the error of a call
549/// past its deadline, so it gives `REQUEST_TIMED_OUT` (7), as Kafka's
550/// `Call.handleTimeoutFailure` gives a `TimeoutException`.
551fn broker_error(broker_id: i32, error: &AdminError) -> KafkaError {
552    if is_connection_failure(error) {
553        return KafkaError {
554            code: REQUEST_TIMED_OUT,
555            name: kafka_error_name(REQUEST_TIMED_OUT),
556            message: Some(format!("the call to broker {broker_id} timed out: {error}")),
557        };
558    }
559    list_groups_kafka_error(error)
560}
561
562/// One attempt of [`call_broker`]. Without a connection, the attempt finds
563/// the broker in the metadata and connects to it. A missing broker or a
564/// connection failure empties `connection` and asks for another attempt.
565async fn broker_attempt<R>(
566    connection: &mut Option<Connection>,
567    conn: &RecoveringConnection,
568    target: NodeTarget,
569    options: &ConnectionOptions,
570    request: R,
571) -> RetryAction<R::Response>
572where
573    R: ProtocolRequest,
574{
575    let broker_id = target.node_id;
576    if connection.is_none() {
577        let endpoint = match node_endpoint(conn, target).await {
578            Ok(Some(endpoint)) => endpoint,
579            Ok(None) => {
580                tracing::debug!(broker_id, "the broker is not in the metadata; retrying");
581                return RetryAction::SameCoordinator(Err(AdminError::Broker {
582                    api: "Metadata",
583                    code: REQUEST_TIMED_OUT,
584                    name: kafka_error_name(REQUEST_TIMED_OUT),
585                    message: Some(format!("broker {broker_id} is not in the metadata")),
586                }));
587            }
588            Err(error) if is_connection_failure(&error) => {
589                return RetryAction::SameCoordinator(Err(error));
590            }
591            Err(error) => return RetryAction::Done(Err(error)),
592        };
593        match AdminClient::connect_one(&endpoint, options.clone()).await {
594            Ok(new) => *connection = Some(new),
595            Err(error) if is_connection_failure(&error) => {
596                return RetryAction::SameCoordinator(Err(error));
597            }
598            Err(error) => return RetryAction::Done(Err(error)),
599        }
600    }
601    let Some(current) = connection.as_ref() else {
602        return RetryAction::SameCoordinator(Err(AdminError::Transport(
603            krabka_client_core::ClientError::Disconnected,
604        )));
605    };
606    let sent = match target.min_version {
607        Some(min_version) => send_connection_at_least(current, request, min_version).await,
608        None => current.send(request).await,
609    };
610    match sent {
611        Ok(response) => RetryAction::Done(Ok(response)),
612        Err(error) => {
613            let error = AdminError::from(error);
614            if is_connection_failure(&error) {
615                *connection = None;
616                RetryAction::SameCoordinator(Err(error))
617            } else {
618                RetryAction::Done(Err(error))
619            }
620        }
621    }
622}
623
624/// The `host:port` of the node of `target`, or `None` when no node has its
625/// id. With a controller bootstrap and `supports_controllers`, the node is
626/// one of the controllers that `DescribeCluster` names, as Kafka's admin
627/// metadata holds the controllers then; otherwise a broker of fresh metadata.
628async fn node_endpoint(
629    conn: &RecoveringConnection,
630    target: NodeTarget,
631) -> Result<Option<String>, AdminError> {
632    if !(target.supports_controllers && conn.uses_controller_bootstrap()) {
633        return broker_endpoint(conn, target.node_id).await;
634    }
635    let response = conn
636        .send_at_least(
637            DescribeClusterRequest {
638                endpoint_type: ENDPOINT_TYPE_CONTROLLERS,
639                ..Default::default()
640            },
641            1,
642        )
643        .await?;
644    if response.error_code != 0 {
645        return Err(AdminError::Broker {
646            api: "DescribeCluster",
647            code: response.error_code,
648            name: kafka_error_name(response.error_code),
649            message: response.error_message,
650        });
651    }
652    Ok(response
653        .brokers
654        .into_iter()
655        .find(|controller| controller.broker_id == target.node_id)
656        .map(|controller| format_host_port(&controller.host, controller.port)))
657}
658
659/// The `host:port` of `broker_id` in fresh metadata, or `None` when the
660/// metadata does not name it.
661async fn broker_endpoint(
662    conn: &RecoveringConnection,
663    broker_id: i32,
664) -> Result<Option<String>, AdminError> {
665    let response = conn
666        .send(MetadataRequest {
667            topics: Some(Vec::new()),
668            allow_auto_topic_creation: true,
669            ..Default::default()
670        })
671        .await?;
672    Ok(response
673        .brokers
674        .into_iter()
675        .find(|broker| broker.node_id == broker_id)
676        .map(|broker| format_host_port(&broker.host, broker.port)))
677}
678
679/// The `AlterReplicaLogDirs` request of one broker. It lists each log dir
680/// once, each topic once per log dir, and the partitions in order.
681fn alter_request(moves: &BTreeMap<TopicPartitionReplica, String>) -> AlterReplicaLogDirsRequest {
682    let mut dirs = BTreeMap::<&str, BTreeMap<&str, Vec<i32>>>::new();
683    for (replica, path) in moves {
684        dirs.entry(path.as_str())
685            .or_default()
686            .entry(replica.topic.as_str())
687            .or_default()
688            .push(replica.partition);
689    }
690    AlterReplicaLogDirsRequest {
691        dirs: dirs
692            .into_iter()
693            .map(|(path, topics)| AlterReplicaLogDir {
694                path: path.to_owned(),
695                topics: topics
696                    .into_iter()
697                    .map(|(name, partitions)| AlterReplicaLogDirTopic {
698                        name: name.to_owned(),
699                        partitions,
700                        ..Default::default()
701                    })
702                    .collect(),
703                ..Default::default()
704            })
705            .collect(),
706        ..Default::default()
707    }
708}
709
710/// The result of each replica that `broker_id` moves. A result for a
711/// partition that is not in the request is skipped, and a replica with no
712/// result gets `UNKNOWN_SERVER_ERROR`, as Kafka does.
713fn alter_results(
714    broker_id: i32,
715    moves: &BTreeMap<TopicPartitionReplica, String>,
716    response: AlterReplicaLogDirsResponse,
717) -> BTreeMap<TopicPartitionReplica, BrokerResult<()>> {
718    let mut out = BTreeMap::new();
719    for topic in response.results {
720        for partition in topic.partitions {
721            let replica = TopicPartitionReplica {
722                topic: topic.topic_name.clone(),
723                partition: partition.partition_index,
724                broker_id,
725            };
726            if !moves.contains_key(&replica) {
727                tracing::warn!(
728                    broker_id,
729                    topic = %replica.topic,
730                    partition = replica.partition,
731                    "the AlterReplicaLogDirs response names a partition that is not in the request"
732                );
733                continue;
734            }
735            let result = kafka_error_if(partition.error_code, None).map_or(Ok(()), Err);
736            out.insert(replica, result);
737        }
738    }
739    for replica in moves.keys() {
740        out.entry(replica.clone()).or_insert_with(|| {
741            Err(KafkaError {
742                code: UNKNOWN_SERVER_ERROR,
743                name: kafka_error_name(UNKNOWN_SERVER_ERROR),
744                message: Some(format!(
745                    "the response from broker {broker_id} did not contain a result for replica \
746                     {}-{}",
747                    replica.topic, replica.partition
748                )),
749            })
750        });
751    }
752    out
753}
754
755fn describe_request(filter: Option<&BTreeMap<String, Vec<i32>>>) -> DescribeLogDirsRequest {
756    DescribeLogDirsRequest {
757        topics: filter.map(|filter| {
758            filter
759                .iter()
760                .map(|(name, partitions)| DescribableLogDirTopic {
761                    topic: name.clone(),
762                    partitions: partitions.clone(),
763                    ..Default::default()
764                })
765                .collect()
766        }),
767        ..Default::default()
768    }
769}
770
771/// A volume size of the response, `None` for `-1`.
772fn volume_bytes(bytes: i64) -> Option<ByteSize> {
773    u64::try_from(bytes).ok().map(ByteSize::from_bytes)
774}
775
776/// The log dirs of one response. No log dir is an error of the broker, as
777/// Kafka's `describeLogDirs` `handleResponse` makes it.
778fn log_dir_infos(response: DescribeLogDirsResponse) -> BrokerResult<Vec<LogDirInfo>> {
779    if response.results.is_empty() {
780        let code = if response.error_code == 0 {
781            CLUSTER_AUTHORIZATION_FAILED
782        } else {
783            response.error_code
784        };
785        return Err(KafkaError {
786            code,
787            name: kafka_error_name(code),
788            message: None,
789        });
790    }
791    Ok(response
792        .results
793        .into_iter()
794        .map(|result| LogDirInfo {
795            log_dir: result.log_dir,
796            error: kafka_error_if(result.error_code, None),
797            topics: result
798                .topics
799                .into_iter()
800                .map(|topic| LogDirTopicInfo {
801                    name: topic.name,
802                    partitions: topic
803                        .partitions
804                        .into_iter()
805                        .map(|partition| LogDirPartitionInfo {
806                            partition_index: partition.partition_index,
807                            partition_size: partition.partition_size,
808                            offset_lag: partition.offset_lag,
809                            is_future_key: partition.is_future_key,
810                        })
811                        .collect(),
812                })
813                .collect(),
814            total: volume_bytes(result.total_bytes),
815            usable: volume_bytes(result.usable_bytes),
816            is_cordoned: result.is_cordoned,
817        })
818        .collect())
819}
820
821#[cfg(test)]
822mod tests {
823    use std::{
824        net::SocketAddr,
825        sync::{Arc, Mutex},
826        time::Duration,
827    };
828
829    use assert2::assert;
830    use bytes::{Buf, BytesMut};
831    use krabka_client_core::MockBroker;
832    use krabka_protocol::{
833        Decode, Encode,
834        owned::{
835            alter_replica_log_dirs_request,
836            alter_replica_log_dirs_response::{
837                AlterReplicaLogDirPartitionResult, AlterReplicaLogDirTopicResult,
838            },
839            api_versions_request,
840            api_versions_response::{ApiVersion, ApiVersionsResponse},
841            describe_log_dirs_request,
842            describe_log_dirs_response::{
843                DescribeLogDirsPartition, DescribeLogDirsResult, DescribeLogDirsTopic,
844            },
845            metadata_request,
846            metadata_response::{MetadataResponse, MetadataResponseBroker},
847        },
848    };
849    use tokio::net::TcpListener;
850
851    use super::*;
852
853    const LONG: Duration = Duration::from_secs(5);
854    const SHORT: Duration = Duration::from_millis(300);
855
856    /// What each mock broker received: the broker id, and the request.
857    #[derive(Clone, Debug, PartialEq, Eq)]
858    enum Received {
859        Describe(i32, DescribeLogDirsRequest),
860        Alter(i32, AlterReplicaLogDirsRequest),
861    }
862
863    /// How one mock broker of the cluster behaves.
864    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
865    enum Behavior {
866        /// It answers for its own log dir and replicas.
867        Normal,
868        /// It answers `DescribeLogDirs` with no log dir and error code 0.
869        NoLogDirs,
870        /// The metadata names it at an address that refuses connections.
871        Down,
872        /// The first metadata response names it at an address that refuses
873        /// connections, and later responses name its real address, as after
874        /// a restart with a new advertised address.
875        Moved,
876    }
877
878    fn encode(response: &impl Encode, version: i16, flexible: bool) -> Vec<u8> {
879        let mut buf = BytesMut::new();
880        if flexible {
881            buf.extend_from_slice(&[0]);
882        }
883        response.encode(&mut buf, version).unwrap();
884        buf.to_vec()
885    }
886
887    fn decode<R: for<'de> Decode<'de>>(mut body: &[u8], version: i16, flexible: bool) -> R {
888        let client_id_len = body.get_i16();
889        body.advance(usize::try_from(client_id_len).expect("client id length"));
890        if flexible {
891            body.advance(1);
892        }
893        R::decode(&mut body, version).expect("request decodes")
894    }
895
896    fn api_versions() -> Vec<u8> {
897        let api = |api_key, max_version| ApiVersion {
898            api_key,
899            min_version: 0,
900            max_version,
901            ..Default::default()
902        };
903        encode(
904            &ApiVersionsResponse {
905                api_keys: vec![
906                    api(api_versions_request::API_KEY, 0),
907                    api(metadata_request::API_KEY, 12),
908                    api(describe_log_dirs_request::API_KEY, 4),
909                    api(alter_replica_log_dirs_request::API_KEY, 2),
910                ],
911                ..Default::default()
912            },
913            0,
914            false,
915        )
916    }
917
918    fn log_dir(broker_id: i32) -> String {
919        format!("/data/broker-{broker_id}")
920    }
921
922    /// The log dir that broker `broker_id` describes: partition
923    /// `broker_id - 1` of `orders`, and a 1 GiB volume.
924    fn described(broker_id: i32) -> Vec<LogDirInfo> {
925        vec![LogDirInfo {
926            log_dir: log_dir(broker_id),
927            error: None,
928            topics: vec![LogDirTopicInfo {
929                name: "orders".into(),
930                partitions: vec![LogDirPartitionInfo {
931                    partition_index: broker_id - 1,
932                    partition_size: 100,
933                    offset_lag: 0,
934                    is_future_key: false,
935                }],
936            }],
937            total: Some(krabka_units::gibibytes(1)),
938            usable: None,
939            is_cordoned: false,
940        }]
941    }
942
943    /// Start one broker of a two-broker cluster. Broker `broker_id` hosts
944    /// partition `broker_id - 1` of `orders`, and `addresses` holds the
945    /// address of each broker id that the metadata names.
946    async fn cluster_broker(
947        broker_id: i32,
948        behavior: Behavior,
949        addresses: Arc<Mutex<Addresses>>,
950        received: Arc<Mutex<Vec<Received>>>,
951    ) -> MockBroker {
952        MockBroker::start(move |api_key, version, _, body| match api_key {
953            api_versions_request::API_KEY => Some(api_versions()),
954            metadata_request::API_KEY => Some(encode(
955                &MetadataResponse {
956                    brokers: addresses
957                        .lock()
958                        .expect("addresses lock")
959                        .next()
960                        .iter()
961                        .map(|(node_id, addr)| MetadataResponseBroker {
962                            node_id: *node_id,
963                            host: addr.ip().to_string(),
964                            port: i32::from(addr.port()),
965                            ..Default::default()
966                        })
967                        .collect(),
968                    ..Default::default()
969                },
970                version,
971                version >= metadata_request::FLEXIBLE_MIN,
972            )),
973            describe_log_dirs_request::API_KEY => {
974                let flexible = version >= describe_log_dirs_request::FLEXIBLE_MIN;
975                let request: DescribeLogDirsRequest = decode(body, version, flexible);
976                received
977                    .lock()
978                    .expect("received lock")
979                    .push(Received::Describe(broker_id, request));
980                let results = if behavior == Behavior::NoLogDirs {
981                    Vec::new()
982                } else {
983                    vec![DescribeLogDirsResult {
984                        log_dir: log_dir(broker_id),
985                        topics: vec![DescribeLogDirsTopic {
986                            name: "orders".into(),
987                            partitions: vec![DescribeLogDirsPartition {
988                                partition_index: broker_id - 1,
989                                partition_size: 100,
990                                ..Default::default()
991                            }],
992                            ..Default::default()
993                        }],
994                        total_bytes: 1 << 30,
995                        usable_bytes: -1,
996                        ..Default::default()
997                    }]
998                };
999                Some(encode(
1000                    &DescribeLogDirsResponse {
1001                        results,
1002                        ..Default::default()
1003                    },
1004                    version,
1005                    flexible,
1006                ))
1007            }
1008            alter_replica_log_dirs_request::API_KEY => {
1009                let flexible = version >= alter_replica_log_dirs_request::FLEXIBLE_MIN;
1010                let request: AlterReplicaLogDirsRequest = decode(body, version, flexible);
1011                let results = request
1012                    .dirs
1013                    .iter()
1014                    .flat_map(|dir| dir.topics.iter())
1015                    .map(|topic| AlterReplicaLogDirTopicResult {
1016                        topic_name: topic.name.clone(),
1017                        partitions: topic
1018                            .partitions
1019                            .iter()
1020                            .map(|partition| AlterReplicaLogDirPartitionResult {
1021                                partition_index: *partition,
1022                                error_code: if *partition == broker_id - 1 {
1023                                    0
1024                                } else {
1025                                    KAFKA_STORAGE_ERROR
1026                                },
1027                                ..Default::default()
1028                            })
1029                            .collect(),
1030                        ..Default::default()
1031                    })
1032                    .collect();
1033                received
1034                    .lock()
1035                    .expect("received lock")
1036                    .push(Received::Alter(broker_id, request));
1037                Some(encode(
1038                    &AlterReplicaLogDirsResponse {
1039                        results,
1040                        ..Default::default()
1041                    },
1042                    version,
1043                    flexible,
1044                ))
1045            }
1046            _ => None,
1047        })
1048        .await
1049        .unwrap()
1050    }
1051
1052    /// The broker addresses that the metadata names. `first` is used for the
1053    /// first metadata response only.
1054    #[derive(Default)]
1055    struct Addresses {
1056        first: Option<Vec<(i32, SocketAddr)>>,
1057        later: Vec<(i32, SocketAddr)>,
1058    }
1059
1060    impl Addresses {
1061        fn next(&mut self) -> Vec<(i32, SocketAddr)> {
1062            self.first.take().unwrap_or_else(|| self.later.clone())
1063        }
1064    }
1065
1066    /// A running two-broker cluster.
1067    struct Cluster {
1068        brokers: Vec<MockBroker>,
1069        received: Arc<Mutex<Vec<Received>>>,
1070        admin: AdminClient,
1071    }
1072
1073    impl Cluster {
1074        /// Start brokers 1 and 2. Broker 1 is the bootstrap broker.
1075        async fn start(broker_2: Behavior) -> Self {
1076            let addresses = Arc::new(Mutex::new(Addresses::default()));
1077            let received = Arc::new(Mutex::new(Vec::new()));
1078            let one = cluster_broker(
1079                1,
1080                Behavior::Normal,
1081                Arc::clone(&addresses),
1082                Arc::clone(&received),
1083            )
1084            .await;
1085            let two =
1086                cluster_broker(2, broker_2, Arc::clone(&addresses), Arc::clone(&received)).await;
1087            let refused = {
1088                let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind a port");
1089                listener.local_addr().expect("local address")
1090            };
1091            *addresses.lock().expect("addresses lock") = match broker_2 {
1092                Behavior::Down => Addresses {
1093                    first: None,
1094                    later: vec![(1, one.addr), (2, refused)],
1095                },
1096                Behavior::Moved => Addresses {
1097                    first: Some(vec![(1, one.addr), (2, refused)]),
1098                    later: vec![(1, one.addr), (2, two.addr)],
1099                },
1100                Behavior::Normal | Behavior::NoLogDirs => Addresses {
1101                    first: None,
1102                    later: vec![(1, one.addr), (2, two.addr)],
1103                },
1104            };
1105            let admin = AdminClient::connect(&[one.addr.to_string()])
1106                .await
1107                .expect("admin connects");
1108            Self {
1109                brokers: vec![one, two],
1110                received,
1111                admin,
1112            }
1113        }
1114
1115        fn stop(self) -> Vec<Received> {
1116            for broker in self.brokers {
1117                broker.stop();
1118            }
1119            let mut received = self.received.lock().expect("received lock").clone();
1120            received.sort_by_key(|entry| match entry {
1121                Received::Describe(id, _) | Received::Alter(id, _) => *id,
1122            });
1123            received
1124        }
1125    }
1126
1127    fn policy(timeout: Duration) -> RetryPolicy {
1128        let backoff = if timeout == LONG {
1129            Duration::from_millis(1)
1130        } else {
1131            timeout
1132        };
1133        RetryPolicy {
1134            timeout,
1135            initial_backoff: backoff,
1136            max_backoff: backoff,
1137            jitter: 0.0,
1138            max_retries: u32::MAX,
1139        }
1140    }
1141
1142    /// The Kafka error code of each broker result.
1143    fn codes<T>(results: BTreeMap<i32, BrokerResult<T>>) -> BTreeMap<i32, Result<T, i16>> {
1144        results
1145            .into_iter()
1146            .map(|(broker_id, result)| (broker_id, result.map_err(|error| error.code)))
1147            .collect()
1148    }
1149
1150    /// Kafka's `describeLogDirs` sends one `DescribeLogDirs` to each named
1151    /// broker (`ConstantNodeIdProvider`), waits for an unknown broker until
1152    /// the deadline, fails a broker that answers with no log dir with its
1153    /// error code or `CLUSTER_AUTHORIZATION_FAILED`, and times out a broker
1154    /// that it cannot reach.
1155    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1156    async fn describe_log_dirs_asks_each_broker_by_id() {
1157        let all = describe_request(None);
1158        for (name, broker_2, brokers, timeout, expected_requests, expected) in [
1159            (
1160                "both brokers",
1161                Behavior::Normal,
1162                vec![1, 2],
1163                LONG,
1164                vec![
1165                    Received::Describe(1, all.clone()),
1166                    Received::Describe(2, all.clone()),
1167                ],
1168                maplit::btreemap! {
1169                    1 => Ok(described(1)),
1170                    2 => Ok(described(2)),
1171                },
1172            ),
1173            (
1174                "one broker",
1175                Behavior::Normal,
1176                vec![2],
1177                LONG,
1178                vec![Received::Describe(2, all.clone())],
1179                maplit::btreemap! {
1180                    2 => Ok(described(2)),
1181                },
1182            ),
1183            (
1184                "an unknown broker times out",
1185                Behavior::Normal,
1186                vec![3],
1187                SHORT,
1188                vec![],
1189                maplit::btreemap! {
1190                    3 => Err(REQUEST_TIMED_OUT),
1191                },
1192            ),
1193            (
1194                "a known broker is asked while an unknown broker waits",
1195                Behavior::Normal,
1196                vec![1, 3],
1197                SHORT,
1198                vec![Received::Describe(1, all.clone())],
1199                maplit::btreemap! {
1200                    1 => Ok(described(1)),
1201                    3 => Err(REQUEST_TIMED_OUT),
1202                },
1203            ),
1204            (
1205                "a broker at a new address is found again",
1206                Behavior::Moved,
1207                vec![2],
1208                LONG,
1209                vec![Received::Describe(2, all.clone())],
1210                maplit::btreemap! {
1211                    2 => Ok(described(2)),
1212                },
1213            ),
1214            (
1215                "a broker with no log dir",
1216                Behavior::NoLogDirs,
1217                vec![1, 2],
1218                LONG,
1219                vec![
1220                    Received::Describe(1, all.clone()),
1221                    Received::Describe(2, all.clone()),
1222                ],
1223                maplit::btreemap! {
1224                    1 => Ok(described(1)),
1225                    2 => Err(CLUSTER_AUTHORIZATION_FAILED),
1226                },
1227            ),
1228            (
1229                "an unreachable broker times out",
1230                Behavior::Down,
1231                vec![1, 2],
1232                SHORT,
1233                vec![Received::Describe(1, all.clone())],
1234                maplit::btreemap! {
1235                    1 => Ok(described(1)),
1236                    2 => Err(REQUEST_TIMED_OUT),
1237                },
1238            ),
1239        ] {
1240            let cluster = Cluster::start(broker_2).await;
1241            let result = cluster
1242                .admin
1243                .describe_log_dirs_with_retry(&brokers, None, policy(timeout))
1244                .await;
1245            let received = cluster.stop();
1246            assert!(
1247                (codes(result), received) == (expected, expected_requests),
1248                "case {name}"
1249            );
1250        }
1251    }
1252
1253    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1254    async fn describe_log_dirs_sends_the_topic_filter() {
1255        let mut cluster = Cluster::start(Behavior::Normal).await;
1256        let filter = maplit::btreemap! {
1257            "orders".to_owned() => vec![0],
1258        };
1259        cluster.admin.describe_log_dirs(&[1], Some(&filter)).await;
1260        let received = cluster.stop();
1261        assert!(
1262            received
1263                == vec![Received::Describe(
1264                    1,
1265                    DescribeLogDirsRequest {
1266                        topics: Some(vec![DescribableLogDirTopic {
1267                            topic: "orders".into(),
1268                            partitions: vec![0],
1269                            ..Default::default()
1270                        }]),
1271                        ..Default::default()
1272                    }
1273                )]
1274        );
1275    }
1276
1277    fn replica(partition: i32, broker_id: i32) -> TopicPartitionReplica {
1278        TopicPartitionReplica {
1279            topic: "orders".into(),
1280            partition,
1281            broker_id,
1282        }
1283    }
1284
1285    fn alter(dirs: &[(&str, &[i32])]) -> AlterReplicaLogDirsRequest {
1286        AlterReplicaLogDirsRequest {
1287            dirs: dirs
1288                .iter()
1289                .map(|(path, partitions)| AlterReplicaLogDir {
1290                    path: (*path).to_owned(),
1291                    topics: vec![AlterReplicaLogDirTopic {
1292                        name: "orders".into(),
1293                        partitions: partitions.to_vec(),
1294                        ..Default::default()
1295                    }],
1296                    ..Default::default()
1297                })
1298                .collect(),
1299            ..Default::default()
1300        }
1301    }
1302
1303    /// Kafka's `alterReplicaLogDirs` groups the moves by
1304    /// `TopicPartitionReplica.brokerId` and sends each group to its broker.
1305    /// A replica with no result in the response fails with
1306    /// `UNKNOWN_SERVER_ERROR`, and a broker that fails fails only its own
1307    /// replicas.
1308    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1309    async fn alter_replica_log_dirs_sends_each_move_to_its_broker() {
1310        for (name, broker_2, moves, timeout, expected_requests, expected) in [
1311            (
1312                "each move goes to the broker of its replica",
1313                Behavior::Normal,
1314                vec![(replica(0, 1), "/a"), (replica(1, 2), "/b")],
1315                LONG,
1316                vec![
1317                    Received::Alter(1, alter(&[("/a", &[0])])),
1318                    Received::Alter(2, alter(&[("/b", &[1])])),
1319                ],
1320                maplit::btreemap! {
1321                    replica(0, 1) => Ok(()),
1322                    replica(1, 2) => Ok(()),
1323                },
1324            ),
1325            (
1326                "a broker groups its moves by log dir",
1327                Behavior::Normal,
1328                vec![
1329                    (replica(0, 1), "/a"),
1330                    (replica(5, 1), "/a"),
1331                    (replica(6, 1), "/b"),
1332                ],
1333                LONG,
1334                vec![Received::Alter(1, alter(&[("/a", &[0, 5]), ("/b", &[6])]))],
1335                maplit::btreemap! {
1336                    replica(0, 1) => Ok(()),
1337                    replica(5, 1) => Err(KAFKA_STORAGE_ERROR),
1338                    replica(6, 1) => Err(KAFKA_STORAGE_ERROR),
1339                },
1340            ),
1341            (
1342                "an unreachable broker fails only its replicas",
1343                Behavior::Down,
1344                vec![(replica(0, 1), "/a"), (replica(1, 2), "/b")],
1345                SHORT,
1346                vec![Received::Alter(1, alter(&[("/a", &[0])]))],
1347                maplit::btreemap! {
1348                    replica(0, 1) => Ok(()),
1349                    replica(1, 2) => Err(REQUEST_TIMED_OUT),
1350                },
1351            ),
1352            (
1353                "an unknown broker times out",
1354                Behavior::Normal,
1355                vec![(replica(0, 9), "/a")],
1356                SHORT,
1357                vec![],
1358                maplit::btreemap! {
1359                    replica(0, 9) => Err(REQUEST_TIMED_OUT),
1360                },
1361            ),
1362        ] {
1363            let cluster = Cluster::start(broker_2).await;
1364            let assignments = moves
1365                .into_iter()
1366                .map(|(replica, path)| (replica, path.to_owned()))
1367                .collect();
1368            let result = cluster
1369                .admin
1370                .alter_replica_log_dirs_with_retry(&assignments, policy(timeout))
1371                .await
1372                .into_iter()
1373                .map(|(replica, result)| (replica, result.map_err(|error| error.code)))
1374                .collect::<BTreeMap<_, _>>();
1375            let received = cluster.stop();
1376            assert!(
1377                (result, received) == (expected, expected_requests),
1378                "case {name}"
1379            );
1380        }
1381    }
1382
1383    fn replica_request_of(broker: &[(&str, &[i32])]) -> DescribeLogDirsRequest {
1384        DescribeLogDirsRequest {
1385            topics: Some(
1386                broker
1387                    .iter()
1388                    .map(|(topic, partitions)| DescribableLogDirTopic {
1389                        topic: (*topic).to_owned(),
1390                        partitions: partitions.to_vec(),
1391                        ..Default::default()
1392                    })
1393                    .collect(),
1394            ),
1395            ..Default::default()
1396        }
1397    }
1398
1399    /// The current log dir of the replica that broker `broker_id` hosts in
1400    /// the mock cluster.
1401    fn hosted(broker_id: i32) -> ReplicaLogDirInfo {
1402        ReplicaLogDirInfo {
1403            current: Some(ReplicaLogDir {
1404                path: log_dir(broker_id),
1405                offset_lag: 0,
1406            }),
1407            future: None,
1408        }
1409    }
1410
1411    /// Kafka's `describeReplicaLogDirs` sends each broker one
1412    /// `DescribeLogDirs` that names only its replicas, finds a moved broker
1413    /// again, gives an empty `ReplicaLogDirInfo` to a replica the broker does
1414    /// not report, and times out a broker it cannot reach.
1415    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1416    async fn describe_replica_log_dirs_asks_the_broker_of_each_replica() {
1417        for (name, broker_2, replicas, timeout, expected_requests, expected) in [
1418            (
1419                "each replica is asked of its broker",
1420                Behavior::Normal,
1421                vec![replica(0, 1), replica(5, 1), replica(1, 2)],
1422                LONG,
1423                vec![
1424                    Received::Describe(1, replica_request_of(&[("orders", &[0, 5])])),
1425                    Received::Describe(2, replica_request_of(&[("orders", &[1])])),
1426                ],
1427                maplit::btreemap! {
1428                    replica(0, 1) => Ok(hosted(1)),
1429                    replica(5, 1) => Ok(ReplicaLogDirInfo::default()),
1430                    replica(1, 2) => Ok(hosted(2)),
1431                },
1432            ),
1433            (
1434                "a broker at a new address is found again",
1435                Behavior::Moved,
1436                vec![replica(1, 2)],
1437                LONG,
1438                vec![Received::Describe(
1439                    2,
1440                    replica_request_of(&[("orders", &[1])]),
1441                )],
1442                maplit::btreemap! {
1443                    replica(1, 2) => Ok(hosted(2)),
1444                },
1445            ),
1446            (
1447                "a broker with no log dir reports no replica",
1448                Behavior::NoLogDirs,
1449                vec![replica(1, 2)],
1450                LONG,
1451                vec![Received::Describe(
1452                    2,
1453                    replica_request_of(&[("orders", &[1])]),
1454                )],
1455                maplit::btreemap! {
1456                    replica(1, 2) => Ok(ReplicaLogDirInfo::default()),
1457                },
1458            ),
1459            (
1460                "an unreachable broker fails the replicas still pending",
1461                Behavior::Down,
1462                vec![replica(0, 1), replica(1, 2)],
1463                SHORT,
1464                vec![Received::Describe(
1465                    1,
1466                    replica_request_of(&[("orders", &[0])]),
1467                )],
1468                maplit::btreemap! {
1469                    replica(0, 1) => Ok(hosted(1)),
1470                    replica(1, 2) => Err(REQUEST_TIMED_OUT),
1471                },
1472            ),
1473        ] {
1474            let cluster = Cluster::start(broker_2).await;
1475            let result = cluster
1476                .admin
1477                .describe_replica_log_dirs_with_retry(&replicas, policy(timeout))
1478                .await
1479                .into_iter()
1480                .map(|(replica, result)| (replica, result.map_err(|error| error.code)))
1481                .collect::<BTreeMap<_, _>>();
1482            let received = cluster.stop();
1483            assert!(
1484                (result, received) == (expected, expected_requests),
1485                "case {name}"
1486            );
1487        }
1488    }
1489
1490    /// How Kafka's `describeReplicaLogDirs` `handleResponse` reads one
1491    /// answer: a future replica fills the future slot, an offline log dir is
1492    /// skipped, any other log-dir error fails the answer, and the top-level
1493    /// error code is not read.
1494    #[test]
1495    fn replica_log_dirs_reads_current_and_future_replicas() {
1496        let dir =
1497            |path: &str, error_code, partitions: Vec<(i32, i64, bool)>| DescribeLogDirsResult {
1498                error_code,
1499                log_dir: path.to_owned(),
1500                topics: vec![DescribeLogDirsTopic {
1501                    name: "orders".into(),
1502                    partitions: partitions
1503                        .into_iter()
1504                        .map(|(partition_index, offset_lag, is_future_key)| {
1505                            DescribeLogDirsPartition {
1506                                partition_index,
1507                                offset_lag,
1508                                is_future_key,
1509                                ..Default::default()
1510                            }
1511                        })
1512                        .collect(),
1513                    ..Default::default()
1514                }],
1515                ..Default::default()
1516            };
1517        let at = |path: &str, offset_lag| {
1518            Some(ReplicaLogDir {
1519                path: path.to_owned(),
1520                offset_lag,
1521            })
1522        };
1523        let asked = maplit::btreemap! {
1524            "orders".to_owned() => maplit::btreeset! {
1525                0,
1526                1,
1527            },
1528        };
1529        for (name, error_code, results, expected) in [
1530            (
1531                "a replica being moved has a current and a future log dir",
1532                0,
1533                vec![
1534                    dir("/a", 0, vec![(0, 0, false), (1, 3, false)]),
1535                    dir("/b", 0, vec![(0, 42, true), (9, 0, false)]),
1536                ],
1537                Ok(maplit::btreemap! {
1538                    ("orders".to_owned(), 0) => ReplicaLogDirInfo {
1539                    current: at("/a", 0),
1540                    future: at("/b", 42),
1541                },
1542                    ("orders".to_owned(), 1) => ReplicaLogDirInfo {
1543                    current: at("/a", 3),
1544                    future: None,
1545                },
1546                }),
1547            ),
1548            (
1549                "an offline log dir is skipped",
1550                0,
1551                vec![
1552                    dir("/offline", KAFKA_STORAGE_ERROR, vec![(0, 0, false)]),
1553                    dir("/a", 0, vec![(1, 0, false)]),
1554                ],
1555                Ok(maplit::btreemap! {
1556                    ("orders".to_owned(), 1) => ReplicaLogDirInfo {
1557                    current: at("/a", 0),
1558                    future: None,
1559                },
1560                }),
1561            ),
1562            (
1563                "another log dir error fails the broker",
1564                0,
1565                vec![dir("/a", 57, vec![])],
1566                Err(KafkaError {
1567                    code: 57,
1568                    name: "LOG_DIR_NOT_FOUND",
1569                    message: Some(
1570                        "the error LOG_DIR_NOT_FOUND for log directory /a in the response from \
1571                         broker 1 is illegal"
1572                            .to_owned(),
1573                    ),
1574                }),
1575            ),
1576            (
1577                "the top-level error is not read",
1578                CLUSTER_AUTHORIZATION_FAILED,
1579                vec![dir("/a", 0, vec![(1, 0, false)])],
1580                Ok(maplit::btreemap! {
1581                    ("orders".to_owned(), 1) => ReplicaLogDirInfo {
1582                    current: at("/a", 0),
1583                    future: None,
1584                },
1585                }),
1586            ),
1587        ] {
1588            let response = DescribeLogDirsResponse {
1589                error_code,
1590                results,
1591                ..Default::default()
1592            };
1593            assert!(
1594                replica_log_dirs(1, &asked, response) == expected,
1595                "case {name}"
1596            );
1597        }
1598    }
1599
1600    /// Kafka's replica futures: an answer completes the pending replicas of
1601    /// its broker, and a failure (an illegal log-dir error or a failed call)
1602    /// completes every replica of the call that is still pending with that
1603    /// error. Replicas that are complete keep their result.
1604    #[test]
1605    fn a_failure_fails_every_replica_still_pending() {
1606        let asked = |partitions: &[i32]| {
1607            maplit::btreemap! {
1608                "orders".to_owned() => partitions.iter().copied().collect::<BTreeSet<_>>(),
1609            }
1610        };
1611        let by_broker = maplit::btreemap! {
1612            1 => asked(&[0, 5]),
1613            2 => asked(&[1]),
1614        };
1615        let found = |partition: i32, broker_id: i32| {
1616            (
1617                ("orders".to_owned(), partition),
1618                ReplicaLogDirInfo {
1619                    current: Some(ReplicaLogDir {
1620                        path: log_dir(broker_id),
1621                        offset_lag: 0,
1622                    }),
1623                    future: None,
1624                },
1625            )
1626        };
1627        let answer = |broker_id: i32, partition: i32| {
1628            let (key, value) = found(partition, broker_id);
1629            (
1630                broker_id,
1631                Ok(maplit::btreemap! {
1632                    key => value,
1633                }),
1634            )
1635        };
1636        let illegal = KafkaError {
1637            code: 57,
1638            name: "LOG_DIR_NOT_FOUND",
1639            message: Some("illegal".to_owned()),
1640        };
1641        let timed_out = KafkaError {
1642            code: REQUEST_TIMED_OUT,
1643            name: "REQUEST_TIMED_OUT",
1644            message: None,
1645        };
1646        let hosted = |partition, broker_id| Ok(found(partition, broker_id).1);
1647        for (name, answers, expected) in [
1648            (
1649                "every broker answers",
1650                vec![answer(2, 1), answer(1, 0)],
1651                maplit::btreemap! {
1652                    replica(0, 1) => hosted(0, 1),
1653                    replica(5, 1) => Ok(ReplicaLogDirInfo::default()),
1654                    replica(1, 2) => hosted(1, 2),
1655                },
1656            ),
1657            (
1658                "an illegal log dir after an answer",
1659                vec![answer(1, 0), (2, Err(illegal.clone()))],
1660                maplit::btreemap! {
1661                    replica(0, 1) => hosted(0, 1),
1662                    replica(5, 1) => Ok(ReplicaLogDirInfo::default()),
1663                    replica(1, 2) => Err(illegal.clone()),
1664                },
1665            ),
1666            (
1667                "an illegal log dir before an answer",
1668                vec![(2, Err(illegal.clone())), answer(1, 0)],
1669                maplit::btreemap! {
1670                    replica(0, 1) => Err(illegal.clone()),
1671                    replica(5, 1) => Err(illegal.clone()),
1672                    replica(1, 2) => Err(illegal),
1673                },
1674            ),
1675            (
1676                "a failed call before an answer",
1677                vec![(1, Err(timed_out.clone())), answer(2, 1)],
1678                maplit::btreemap! {
1679                    replica(0, 1) => Err(timed_out.clone()),
1680                    replica(5, 1) => Err(timed_out.clone()),
1681                    replica(1, 2) => Err(timed_out),
1682                },
1683            ),
1684        ] {
1685            let mut results = ReplicaResults::new(&by_broker);
1686            let mut completed_after = Vec::new();
1687            for (broker_id, answer) in answers {
1688                results.complete(broker_id, &by_broker[&broker_id], answer);
1689                completed_after.push(results.is_complete());
1690            }
1691            assert!(
1692                (completed_after.last().copied(), results.finish()) == (Some(true), expected),
1693                "case {name}"
1694            );
1695        }
1696    }
1697
1698    #[test]
1699    fn a_replica_without_a_result_fails_with_unknown_server_error() {
1700        let moves = maplit::btreemap! {
1701            replica(0, 1) => "/a".to_owned(),
1702            replica(1, 1) => "/a".to_owned(),
1703        };
1704        let response = AlterReplicaLogDirsResponse {
1705            results: vec![AlterReplicaLogDirTopicResult {
1706                topic_name: "orders".into(),
1707                partitions: vec![
1708                    AlterReplicaLogDirPartitionResult {
1709                        partition_index: 0,
1710                        ..Default::default()
1711                    },
1712                    AlterReplicaLogDirPartitionResult {
1713                        partition_index: 7,
1714                        ..Default::default()
1715                    },
1716                ],
1717                ..Default::default()
1718            }],
1719            ..Default::default()
1720        };
1721        assert!(
1722            alter_results(1, &moves, response)
1723                == maplit::btreemap! {
1724                    replica(0, 1) => Ok(()),
1725                    replica(1, 1) => Err(KafkaError {
1726                    code: UNKNOWN_SERVER_ERROR,
1727                    name: "UNKNOWN_SERVER_ERROR",
1728                    message: Some(
1729                        "the response from broker 1 did not contain a result for replica \
1730                         orders-1"
1731                            .into()
1732                    ),
1733                }),
1734                }
1735        );
1736    }
1737
1738    #[test]
1739    fn a_log_dir_reports_its_volume_sizes() {
1740        let response = DescribeLogDirsResponse {
1741            results: vec![DescribeLogDirsResult {
1742                log_dir: "/d".into(),
1743                error_code: 57,
1744                total_bytes: 4096,
1745                usable_bytes: 1024,
1746                is_cordoned: true,
1747                ..Default::default()
1748            }],
1749            ..Default::default()
1750        };
1751        assert!(
1752            log_dir_infos(response)
1753                == Ok(vec![LogDirInfo {
1754                    log_dir: "/d".into(),
1755                    error: kafka_error_if(57, None),
1756                    topics: vec![],
1757                    total: Some(krabka_units::kibibytes(4)),
1758                    usable: Some(krabka_units::kibibytes(1)),
1759                    is_cordoned: true,
1760                }])
1761        );
1762    }
1763}