1use 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
40const UNKNOWN_SERVER_ERROR: i16 = -1;
42const REQUEST_TIMED_OUT: i16 = 7;
44const CLUSTER_AUTHORIZATION_FAILED: i16 = 31;
46const KAFKA_STORAGE_ERROR: i16 = 56;
49const ENDPOINT_TYPE_CONTROLLERS: i8 = 2;
51
52#[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#[derive(Debug, Clone, PartialEq)]
64pub struct LogDirInfo {
65 pub log_dir: String,
66 pub error: Option<KafkaError>,
67 pub topics: Vec<LogDirTopicInfo>,
68 pub total: Option<ByteSize>,
71 pub usable: Option<ByteSize>,
74 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
92pub type BrokerResult<T> = Result<T, KafkaError>;
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ReplicaLogDir {
98 pub path: String,
100 pub offset_lag: i64,
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq)]
110pub struct ReplicaLogDirInfo {
111 pub current: Option<ReplicaLogDir>,
113 pub future: Option<ReplicaLogDir>,
116}
117
118impl AdminClient {
119 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 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 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#[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 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
397fn 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
415fn 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
473async 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496pub(crate) struct NodeTarget {
497 pub(crate) node_id: i32,
499 pub(crate) supports_controllers: bool,
504 pub(crate) min_version: Option<i16>,
506}
507
508impl NodeTarget {
509 const fn broker(node_id: i32) -> Self {
511 Self {
512 node_id,
513 supports_controllers: false,
514 min_version: None,
515 }
516 }
517}
518
519pub(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
548fn 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
562async 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
624async 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
659async 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
679fn 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
710fn 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
771fn volume_bytes(bytes: i64) -> Option<ByteSize> {
773 u64::try_from(bytes).ok().map(ByteSize::from_bytes)
774}
775
776fn 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 #[derive(Clone, Debug, PartialEq, Eq)]
858 enum Received {
859 Describe(i32, DescribeLogDirsRequest),
860 Alter(i32, AlterReplicaLogDirsRequest),
861 }
862
863 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
865 enum Behavior {
866 Normal,
868 NoLogDirs,
870 Down,
872 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 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 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 #[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 struct Cluster {
1068 brokers: Vec<MockBroker>,
1069 received: Arc<Mutex<Vec<Received>>>,
1070 admin: AdminClient,
1071 }
1072
1073 impl Cluster {
1074 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 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 #[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 #[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 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 #[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 #[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 #[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}