1use std::{
4 collections::{BTreeMap, HashMap, HashSet},
5 time::Duration,
6};
7
8#[cfg(test)]
9use krabka_protocol::owned::metadata_response;
10use krabka_protocol::{
11 owned::{
12 alter_partition_reassignments_request::{
13 AlterPartitionReassignmentsRequest, ReassignablePartition, ReassignableTopic,
14 },
15 alter_partition_reassignments_response::AlterPartitionReassignmentsResponse,
16 create_partitions_request::{
17 CreatePartitionsAssignment, CreatePartitionsRequest, CreatePartitionsTopic,
18 },
19 create_topics_request::{
20 CreatableReplicaAssignment, CreatableTopic, CreatableTopicConfig, CreateTopicsRequest,
21 },
22 delete_records_request::{
23 DeleteRecordsPartition, DeleteRecordsRequest, DeleteRecordsTopic,
24 },
25 delete_topics_request::{DeleteTopicState, DeleteTopicsRequest},
26 describe_cluster_request::DescribeClusterRequest,
27 list_partition_reassignments_request::{
28 ListPartitionReassignmentsRequest, ListPartitionReassignmentsTopics,
29 },
30 list_partition_reassignments_response::ListPartitionReassignmentsResponse,
31 metadata_request::{MetadataRequest, MetadataRequestTopic},
32 metadata_response::MetadataResponse,
33 },
34 primitives::uuid::Uuid as ProtoUuid,
35};
36use krabka_units::{Time, convert::TimeExt as _};
37use tokio::time::Instant;
38use uuid::Uuid;
39
40use crate::{
41 AdminClient, AdminError, KafkaError, NOT_CONTROLLER, kafka_error_if,
42 retry::{ControllerRetry, RetryPolicy, call_timeout_error, is_connection_failure},
43};
44
45#[derive(Debug, Clone, Default, bon::Builder)]
47pub struct CreateTopicSpec {
48 #[builder(into)]
49 pub name: String,
50 #[builder(default = -1)]
52 pub partitions: i32,
53 #[builder(default = -1)]
55 pub replicas: i32,
56 #[builder(default)]
57 pub configs: BTreeMap<String, String>,
58 #[builder(default)]
63 pub replica_assignments: BTreeMap<i32, Vec<i32>>,
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct CreateTopicOutcome {
68 pub name: String,
69 pub topic_id: Option<Uuid>,
70 pub error: Option<KafkaError>,
71 pub throttle_time: Option<Time>,
75}
76
77#[derive(Debug, Clone, PartialEq)]
78pub struct DeleteTopicOutcome {
79 pub name: String,
80 pub error: Option<KafkaError>,
81 pub throttle_time: Option<Time>,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct TopicMutationOptions {
94 pub timeout: Option<Time>,
98 pub retry_on_quota_violation: bool,
101}
102
103impl Default for TopicMutationOptions {
104 fn default() -> Self {
105 Self {
106 timeout: None,
107 retry_on_quota_violation: true,
108 }
109 }
110}
111
112impl TopicMutationOptions {
113 #[must_use]
115 pub fn with_timeout(timeout: Time) -> Self {
116 Self {
117 timeout: Some(timeout),
118 ..Self::default()
119 }
120 }
121
122 fn retry_policy(self, base: RetryPolicy) -> RetryPolicy {
124 self.timeout.map_or(base, |timeout| RetryPolicy {
125 timeout: Duration::from_millis(u64::try_from(timeout.millis_i64()).unwrap_or(0)),
126 ..base
127 })
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct DeleteRecordsOp {
133 pub topic: String,
134 pub partition: i32,
135 pub offset: i64,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct DeleteRecordsOutcome {
140 pub topic: String,
141 pub partition: i32,
142 pub error_code: i16,
143 pub low_watermark: i64,
144}
145
146#[derive(Debug, Clone, Default)]
148pub struct CreatePartitionsOp {
149 pub name: String,
150 pub new_total_count: i32,
151 pub assignments: Option<Vec<Vec<i32>>>,
155}
156
157#[derive(Debug, Clone, PartialEq)]
158pub struct CreatePartitionsOutcome {
159 pub name: String,
160 pub error: Option<KafkaError>,
161 pub throttle_time: Option<Time>,
164}
165
166#[derive(Debug, Clone, Default, PartialEq, Eq)]
167pub struct TopicMetadata {
168 pub controller_id: i32,
169 pub topics: Vec<TopicMetadataEntry>,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct TopicMetadataEntry {
174 pub name: String,
175 pub topic_id: Option<Uuid>,
176 pub partition_count: i32,
177 pub replication_factor: i32,
178 pub error: Option<KafkaError>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum TopicReplicationStatus {
185 InSync,
187 ReassignmentInProgress,
189 ReassignmentSubmitted,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct PartitionAssignment {
195 pub topic: String,
196 pub partition: i32,
197 pub replicas: Vec<i32>,
198 pub adding_replicas: Vec<i32>,
199 pub removing_replicas: Vec<i32>,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct PartitionAssignmentOutcome {
204 pub topic: String,
205 pub partition: i32,
206 pub error: Option<KafkaError>,
207}
208
209impl AdminClient {
210 pub(crate) async fn describe_partition_assignments_impl(
212 &self,
213 topics: &[&str],
214 ) -> Result<Vec<PartitionAssignment>, AdminError> {
215 let response: MetadataResponse = self.conn.send(build_metadata(topics)).await?;
216 Ok(response
217 .topics
218 .into_iter()
219 .flat_map(|topic| {
220 let name = topic.name.unwrap_or_default();
221 topic
222 .partitions
223 .into_iter()
224 .map(move |partition| PartitionAssignment {
225 topic: name.clone(),
226 partition: partition.partition_index,
227 replicas: partition.replica_nodes,
228 adding_replicas: Vec::new(),
229 removing_replicas: Vec::new(),
230 })
231 })
232 .collect())
233 }
234
235 pub(crate) async fn list_partition_reassignments_impl(
237 &self,
238 partitions: &BTreeMap<String, Vec<i32>>,
239 timeout: Time,
240 ) -> Result<Vec<PartitionAssignment>, AdminError> {
241 let response = self
242 .conn
243 .send(ListPartitionReassignmentsRequest {
244 timeout_ms: timeout.millis_i32(),
245 topics: (!partitions.is_empty()).then(|| {
246 partitions
247 .iter()
248 .map(|(name, indexes)| ListPartitionReassignmentsTopics {
249 name: name.clone(),
250 partition_indexes: indexes.clone(),
251 ..Default::default()
252 })
253 .collect()
254 }),
255 ..Default::default()
256 })
257 .await?;
258 broker_error(
259 "ListPartitionReassignments",
260 response.error_code,
261 response.error_message,
262 )?;
263 Ok(response
264 .topics
265 .into_iter()
266 .flat_map(|topic| {
267 topic
268 .partitions
269 .into_iter()
270 .map(move |partition| PartitionAssignment {
271 topic: topic.name.clone(),
272 partition: partition.partition_index,
273 replicas: partition.replicas,
274 adding_replicas: partition.adding_replicas,
275 removing_replicas: partition.removing_replicas,
276 })
277 })
278 .collect())
279 }
280
281 pub(crate) async fn alter_partition_assignments_impl(
283 &mut self,
284 assignments: &BTreeMap<(String, i32), Option<Vec<i32>>>,
285 timeout: Time,
286 ) -> Result<Vec<PartitionAssignmentOutcome>, AdminError> {
287 self.alter_partition_assignments_with(
288 assignments,
289 timeout,
290 AlterPartitionReassignmentsOptions::default(),
291 )
292 .await
293 }
294
295 pub async fn alter_partition_assignments_with(
304 &mut self,
305 assignments: &BTreeMap<(String, i32), Option<Vec<i32>>>,
306 timeout: Time,
307 options: AlterPartitionReassignmentsOptions,
308 ) -> Result<Vec<PartitionAssignmentOutcome>, AdminError> {
309 let request = build_partition_assignment_request(assignments, timeout, options);
310 let min_version = if options.allow_replication_factor_change {
311 0
312 } else {
313 ALLOW_REPLICATION_FACTOR_CHANGE_MIN_VERSION
314 };
315 let mut retry = ControllerRetry::new("AlterPartitionReassignments", self.retry);
316 loop {
317 let response = retry
318 .bounded(self.conn.send_at_least(request.clone(), min_version))
319 .await?;
320 if response.error_code != NOT_CONTROLLER {
321 return parse_partition_assignment_response(response);
322 }
323 retry.after_not_controller(self).await?;
324 }
325 }
326
327 pub(crate) async fn metadata_impl(&self, topics: &[&str]) -> Result<TopicMetadata, AdminError> {
329 let req = build_metadata(topics);
330 let resp = self.conn.send(req).await?;
331 Ok(parse_metadata(resp))
332 }
333
334 pub(crate) async fn reconcile_topic_replication_factor_impl(
336 &self,
337 topic: &str,
338 replication_factor: i32,
339 timeout: Time,
340 ) -> Result<TopicReplicationStatus, AdminError> {
341 if self.conn.uses_controller_bootstrap() {
342 return Err(AdminError::Broker {
343 api: "ControllerEndpoint",
344 code: 115,
345 name: "UNSUPPORTED_ENDPOINT_TYPE",
346 message: Some(
347 "replication-factor reconciliation requires a broker bootstrap endpoint".into(),
348 ),
349 });
350 }
351 let mut retry = ControllerRetry::new("AlterPartitionReassignments", self.retry);
357 retry.bounded(self.refresh_controller_connection()).await?;
358 loop {
359 match retry
360 .bounded(self.reconcile_topic_replication_factor_once(
361 topic,
362 replication_factor,
363 timeout,
364 ))
365 .await
366 {
367 Err(AdminError::Broker {
368 code: NOT_CONTROLLER,
369 ..
370 }) => retry.after_not_controller(self).await?,
371 result => return result,
372 }
373 }
374 }
375
376 async fn reconcile_topic_replication_factor_once(
377 &self,
378 topic: &str,
379 replication_factor: i32,
380 timeout: Time,
381 ) -> Result<TopicReplicationStatus, AdminError> {
382 let ongoing: ListPartitionReassignmentsResponse = self
383 .conn
384 .send(ListPartitionReassignmentsRequest::default())
385 .await?;
386 broker_error(
387 "ListPartitionReassignments",
388 ongoing.error_code,
389 ongoing.error_message,
390 )?;
391 if ongoing.topics.iter().any(|entry| entry.name == topic) {
392 return Ok(TopicReplicationStatus::ReassignmentInProgress);
393 }
394
395 let cluster = self
399 .conn
400 .send_at_least(
401 DescribeClusterRequest {
402 include_fenced_brokers: true,
403 ..Default::default()
404 },
405 2,
406 )
407 .await?;
408 broker_error("DescribeCluster", cluster.error_code, cluster.error_message)?;
409 let eligible_brokers = cluster
410 .brokers
411 .iter()
412 .filter(|broker| !broker.is_fenced)
413 .map(|broker| broker.broker_id)
414 .collect::<Vec<_>>();
415
416 let metadata: MetadataResponse = self.conn.send(build_metadata(&[topic])).await?;
417 let Some(request) = build_replication_factor_reassignment(
418 &metadata,
419 &eligible_brokers,
420 topic,
421 replication_factor,
422 timeout,
423 )?
424 else {
425 return Ok(TopicReplicationStatus::InSync);
426 };
427 let response: AlterPartitionReassignmentsResponse = self.conn.send(request).await?;
428 broker_error(
429 "AlterPartitionReassignments",
430 response.error_code,
431 response.error_message,
432 )?;
433 if let Some(error) = response.responses.into_iter().find_map(|topic_response| {
434 topic_response
435 .partitions
436 .into_iter()
437 .find(|partition| partition.error_code != 0)
438 }) {
439 return Err(AdminError::Broker {
440 api: "AlterPartitionReassignments",
441 code: error.error_code,
442 name: crate::kafka_error_name(error.error_code),
443 message: error.error_message,
444 });
445 }
446 Ok(TopicReplicationStatus::ReassignmentSubmitted)
447 }
448
449 pub(crate) async fn create_topics_impl(
451 &self,
452 specs: &[CreateTopicSpec],
453 options: TopicMutationOptions,
454 ) -> Result<Vec<CreateTopicOutcome>, AdminError> {
455 self.create_topics_with_retry(specs, options, options.retry_policy(self.retry))
456 .await
457 }
458
459 async fn create_topics_with_retry(
460 &self,
461 specs: &[CreateTopicSpec],
462 options: TopicMutationOptions,
463 policy: RetryPolicy,
464 ) -> Result<Vec<CreateTopicOutcome>, AdminError> {
465 let names = specs.iter().map(|spec| spec.name.clone()).collect();
466 self.mutate_topics(
467 "CreateTopics",
468 names,
469 options.retry_on_quota_violation,
470 policy,
471 |pending, timeout_ms| {
472 let specs = specs
473 .iter()
474 .filter(|spec| pending.contains(&spec.name))
475 .cloned()
476 .collect::<Vec<_>>();
477 build_create_topics(&specs, timeout_ms)
478 },
479 parse_create_topics,
480 )
481 .await
482 }
483
484 pub(crate) async fn delete_topics_impl(
486 &self,
487 names: &[&str],
488 options: TopicMutationOptions,
489 ) -> Result<Vec<DeleteTopicOutcome>, AdminError> {
490 self.delete_topics_with_retry(names, options, options.retry_policy(self.retry))
491 .await
492 }
493
494 async fn delete_topics_with_retry(
495 &self,
496 names: &[&str],
497 options: TopicMutationOptions,
498 policy: RetryPolicy,
499 ) -> Result<Vec<DeleteTopicOutcome>, AdminError> {
500 let names = names.iter().map(|name| (*name).to_owned()).collect();
501 self.mutate_topics(
502 "DeleteTopics",
503 names,
504 options.retry_on_quota_violation,
505 policy,
506 build_delete_topics,
507 parse_delete_topics,
508 )
509 .await
510 }
511
512 pub(crate) async fn create_partitions_impl(
514 &self,
515 ops: &[CreatePartitionsOp],
516 options: TopicMutationOptions,
517 ) -> Result<Vec<CreatePartitionsOutcome>, AdminError> {
518 self.create_partitions_with_retry(ops, options, options.retry_policy(self.retry))
519 .await
520 }
521
522 async fn create_partitions_with_retry(
523 &self,
524 ops: &[CreatePartitionsOp],
525 options: TopicMutationOptions,
526 policy: RetryPolicy,
527 ) -> Result<Vec<CreatePartitionsOutcome>, AdminError> {
528 let names = ops.iter().map(|op| op.name.clone()).collect();
529 self.mutate_topics(
530 "CreatePartitions",
531 names,
532 options.retry_on_quota_violation,
533 policy,
534 |pending, timeout_ms| {
535 let ops = ops
536 .iter()
537 .filter(|op| pending.contains(&op.name))
538 .cloned()
539 .collect::<Vec<_>>();
540 build_create_partitions(&ops, timeout_ms)
541 },
542 parse_create_partitions,
543 )
544 .await
545 }
546
547 async fn mutate_topics<R, T>(
550 &self,
551 api: &'static str,
552 topics: Vec<String>,
553 retry_on_quota_violation: bool,
554 policy: RetryPolicy,
555 build: impl Fn(&[String], i32) -> R,
556 parse: impl Fn(R::Response) -> Vec<T>,
557 ) -> Result<Vec<T>, AdminError>
558 where
559 R: krabka_protocol::ProtocolRequest + Clone,
560 R::Response: 'static,
561 T: TopicMutationOutcome,
562 {
563 let mut order = Vec::new();
564 for topic in topics {
565 if !order.contains(&topic) {
566 order.push(topic);
567 }
568 }
569 let mut pending = order.clone();
570 let mut done = HashMap::<String, T>::new();
571 let mut throttled = HashMap::<String, (T, Instant)>::new();
572 let mut deadline = policy.start();
573 let mut attempts = 0_u32;
574 let mut last_error = "none".to_owned();
575 while !pending.is_empty() {
576 attempts = attempts.saturating_add(1);
577 let request = build(&pending, deadline.remaining_millis());
578 let Some(response) = deadline.bounded(self.conn.send(request)).await else {
581 "the request was in flight at the deadline".clone_into(&mut last_error);
582 break;
583 };
584 let outcomes = match response {
585 Ok(response) => parse(response),
586 Err(error) if is_connection_failure(&error) => {
589 last_error = error.to_string();
590 if deadline.exhausted() {
591 break;
592 }
593 deadline.backoff().await;
594 if deadline.expired() {
595 break;
596 }
597 continue;
598 }
599 Err(error) => return Err(error),
600 };
601 let controller_bootstrap = self.conn.uses_controller_bootstrap();
602 if outcomes
603 .iter()
604 .any(|outcome| is_not_controller(outcome.error_code(), controller_bootstrap))
605 {
606 "NOT_CONTROLLER".clone_into(&mut last_error);
607 if deadline
608 .bounded(self.refresh_controller_after_not_controller())
609 .await
610 .transpose()?
611 .is_none()
612 {
613 break;
614 }
615 if deadline.exhausted() {
616 break;
617 }
618 deadline.backoff().await;
619 if deadline.expired() {
620 break;
621 }
622 continue;
623 }
624
625 let received = Instant::now();
626 let mut retry = Vec::new();
627 let mut throttle = Duration::ZERO;
628 for outcome in outcomes {
629 let topic = outcome.topic().to_owned();
630 if !pending.contains(&topic) || done.contains_key(&topic) {
631 tracing::warn!(api, topic, "server response mentioned an unknown topic");
632 continue;
633 }
634 if retry_on_quota_violation && outcome.error_code() == THROTTLING_QUOTA_EXCEEDED {
635 throttle = throttle.max(outcome.throttle_duration());
636 retry.push(topic.clone());
637 throttled.insert(topic, (outcome, received));
638 } else {
639 throttled.remove(&topic);
640 done.insert(topic, outcome);
641 }
642 }
643 for topic in &pending {
644 if !done.contains_key(topic) && !retry.contains(topic) {
645 let error = KafkaError {
646 code: UNKNOWN_SERVER_ERROR,
647 name: crate::kafka_error_name(UNKNOWN_SERVER_ERROR),
648 message: Some(format!(
649 "The controller response did not contain a result for topic {topic}"
650 )),
651 };
652 done.insert(topic.clone(), T::failed(topic, error));
653 }
654 }
655 pending = retry;
656 if pending.is_empty() {
657 break;
658 }
659 "THROTTLING_QUOTA_EXCEEDED".clone_into(&mut last_error);
660 if throttle.is_zero() {
665 deadline.backoff().await;
666 } else {
667 deadline.sleep(throttle).await;
668 }
669 if deadline.expired() {
670 break;
671 }
672 }
673 for topic in pending {
674 let outcome = match throttled.remove(&topic) {
675 Some((outcome, received)) => {
676 let remaining = outcome
677 .throttle_duration()
678 .saturating_sub(received.elapsed());
679 outcome.with_throttle_duration(remaining)
680 }
681 None => T::failed(&topic, call_timeout_error(api, attempts, &last_error)),
682 };
683 done.insert(topic, outcome);
684 }
685 Ok(order
686 .into_iter()
687 .filter_map(|topic| done.remove(&topic))
688 .collect())
689 }
690
691 pub(crate) async fn delete_records_impl(
693 &self,
694 ops: &[DeleteRecordsOp],
695 timeout: Time,
696 ) -> Result<Vec<DeleteRecordsOutcome>, AdminError> {
697 if ops.is_empty() {
698 return Ok(Vec::new());
699 }
700
701 let mut outcomes = Vec::new();
702 let groups = self
703 .delete_records_leader_groups(ops, &mut outcomes)
704 .await?;
705 for (endpoint, leader_ops) in groups {
706 self.reconnect(&endpoint).await?;
707 let req = build_delete_records(&leader_ops, timeout);
708 let resp = self.conn.send(req).await?;
709 outcomes.extend(parse_delete_records(resp));
710 }
711 Ok(outcomes)
712 }
713
714 pub(crate) async fn refresh_controller_connection(&self) -> Result<(), AdminError> {
718 if self.conn.uses_controller_bootstrap() {
719 return self.conn.rebootstrap().await;
720 }
721 let md_resp = self.conn.send(build_metadata(&[])).await?;
722 let Some(controller_addr) = controller_endpoint(&md_resp) else {
723 if controller_requires_bootstrap_fallback(&md_resp) {
727 return self.conn.rebootstrap().await;
728 }
729 return Err(AdminError::Protocol(format!(
730 "metadata names no endpoint for controller {}",
731 md_resp.controller_id
732 )));
733 };
734 self.reconnect(&controller_addr).await
735 }
736
737 async fn delete_records_leader_groups(
738 &self,
739 ops: &[DeleteRecordsOp],
740 outcomes: &mut Vec<DeleteRecordsOutcome>,
741 ) -> Result<BTreeMap<String, Vec<DeleteRecordsOp>>, AdminError> {
742 let mut topic_names = ops.iter().map(|op| op.topic.as_str()).collect::<Vec<_>>();
743 topic_names.sort_unstable();
744 topic_names.dedup();
745
746 let metadata = self.conn.send(build_metadata(&topic_names)).await?;
747 let broker_endpoints = metadata
748 .brokers
749 .iter()
750 .map(|broker| {
751 let endpoint = if broker.port > 0 {
752 format!("{}:{}", broker.host, broker.port)
753 } else {
754 self.bootstrap_addrs
755 .first()
756 .cloned()
757 .unwrap_or_else(|| format!("{}:{}", broker.host, broker.port))
758 };
759 (broker.node_id, endpoint)
760 })
761 .collect::<BTreeMap<_, _>>();
762 let mut groups = BTreeMap::<String, Vec<DeleteRecordsOp>>::new();
763
764 for op in ops {
765 let Some(topic) = metadata
766 .topics
767 .iter()
768 .find(|topic| topic.name.as_deref() == Some(op.topic.as_str()))
769 else {
770 outcomes.push(delete_records_error_outcome(op, 3));
771 continue;
772 };
773
774 if topic.error_code != 0 {
775 outcomes.push(delete_records_error_outcome(op, topic.error_code));
776 continue;
777 }
778
779 let Some(partition) = topic
780 .partitions
781 .iter()
782 .find(|partition| partition.partition_index == op.partition)
783 else {
784 outcomes.push(delete_records_error_outcome(op, 3));
785 continue;
786 };
787
788 if partition.error_code != 0 {
789 outcomes.push(delete_records_error_outcome(op, partition.error_code));
790 continue;
791 }
792
793 let Some(endpoint) = broker_endpoints.get(&partition.leader_id) else {
794 return Err(AdminError::Protocol(format!(
795 "no broker endpoint for DeleteRecords leader {}",
796 partition.leader_id
797 )));
798 };
799 groups.entry(endpoint.clone()).or_default().push(op.clone());
800 }
801
802 Ok(groups)
803 }
804}
805
806const UNKNOWN_SERVER_ERROR: i16 = -1;
808const NOT_LEADER_OR_FOLLOWER: i16 = 6;
811const THROTTLING_QUOTA_EXCEEDED: i16 = 89;
813
814fn is_not_controller(code: i16, controller_bootstrap: bool) -> bool {
818 code == NOT_CONTROLLER || (controller_bootstrap && code == NOT_LEADER_OR_FOLLOWER)
819}
820
821trait TopicMutationOutcome {
823 fn topic(&self) -> &str;
825 fn error_code(&self) -> i16;
827 fn throttle_duration(&self) -> Duration;
829 #[must_use]
831 fn with_throttle_duration(self, remaining: Duration) -> Self;
832 fn failed(topic: &str, error: KafkaError) -> Self;
834}
835
836fn throttle_duration(throttle_time: Option<Time>) -> Duration {
837 throttle_time.map_or(Duration::ZERO, |time| {
838 Duration::from_millis(u64::try_from(time.millis_i64()).unwrap_or(0))
839 })
840}
841
842fn throttle_time(remaining: Duration) -> Time {
843 Time::from_millis(i64::try_from(remaining.as_millis()).unwrap_or(i64::MAX))
844}
845
846fn quota_throttle_time(code: i16, throttle_time_ms: i32) -> Option<Time> {
849 (code == THROTTLING_QUOTA_EXCEEDED).then(|| Time::from_millis(i64::from(throttle_time_ms)))
850}
851
852impl TopicMutationOutcome for CreateTopicOutcome {
853 fn topic(&self) -> &str {
854 &self.name
855 }
856 fn error_code(&self) -> i16 {
857 self.error.as_ref().map_or(0, |error| error.code)
858 }
859 fn throttle_duration(&self) -> Duration {
860 throttle_duration(self.throttle_time)
861 }
862 fn with_throttle_duration(self, remaining: Duration) -> Self {
863 Self {
864 throttle_time: Some(throttle_time(remaining)),
865 ..self
866 }
867 }
868 fn failed(topic: &str, error: KafkaError) -> Self {
869 Self {
870 name: topic.to_owned(),
871 topic_id: None,
872 error: Some(error),
873 throttle_time: None,
874 }
875 }
876}
877
878impl TopicMutationOutcome for DeleteTopicOutcome {
879 fn topic(&self) -> &str {
880 &self.name
881 }
882 fn error_code(&self) -> i16 {
883 self.error.as_ref().map_or(0, |error| error.code)
884 }
885 fn throttle_duration(&self) -> Duration {
886 throttle_duration(self.throttle_time)
887 }
888 fn with_throttle_duration(self, remaining: Duration) -> Self {
889 Self {
890 throttle_time: Some(throttle_time(remaining)),
891 ..self
892 }
893 }
894 fn failed(topic: &str, error: KafkaError) -> Self {
895 Self {
896 name: topic.to_owned(),
897 error: Some(error),
898 throttle_time: None,
899 }
900 }
901}
902
903impl TopicMutationOutcome for CreatePartitionsOutcome {
904 fn topic(&self) -> &str {
905 &self.name
906 }
907 fn error_code(&self) -> i16 {
908 self.error.as_ref().map_or(0, |error| error.code)
909 }
910 fn throttle_duration(&self) -> Duration {
911 throttle_duration(self.throttle_time)
912 }
913 fn with_throttle_duration(self, remaining: Duration) -> Self {
914 Self {
915 throttle_time: Some(throttle_time(remaining)),
916 ..self
917 }
918 }
919 fn failed(topic: &str, error: KafkaError) -> Self {
920 Self {
921 name: topic.to_owned(),
922 error: Some(error),
923 throttle_time: None,
924 }
925 }
926}
927
928#[derive(Debug, Clone, Copy, PartialEq, Eq)]
930pub struct AlterPartitionReassignmentsOptions {
931 pub allow_replication_factor_change: bool,
934}
935
936impl Default for AlterPartitionReassignmentsOptions {
937 fn default() -> Self {
938 Self {
939 allow_replication_factor_change: true,
940 }
941 }
942}
943
944const ALLOW_REPLICATION_FACTOR_CHANGE_MIN_VERSION: i16 = 1;
947
948fn build_partition_assignment_request(
949 assignments: &BTreeMap<(String, i32), Option<Vec<i32>>>,
950 timeout: Time,
951 options: AlterPartitionReassignmentsOptions,
952) -> AlterPartitionReassignmentsRequest {
953 let mut topics = BTreeMap::<String, Vec<ReassignablePartition>>::new();
954 for ((topic, partition), replicas) in assignments {
955 topics
956 .entry(topic.clone())
957 .or_default()
958 .push(ReassignablePartition {
959 partition_index: *partition,
960 replicas: replicas.clone(),
961 ..Default::default()
962 });
963 }
964 AlterPartitionReassignmentsRequest {
965 timeout_ms: timeout.millis_i32(),
966 allow_replication_factor_change: options.allow_replication_factor_change,
967 topics: topics
968 .into_iter()
969 .map(|(name, partitions)| ReassignableTopic {
970 name,
971 partitions,
972 ..Default::default()
973 })
974 .collect(),
975 ..Default::default()
976 }
977}
978
979fn parse_partition_assignment_response(
980 response: AlterPartitionReassignmentsResponse,
981) -> Result<Vec<PartitionAssignmentOutcome>, AdminError> {
982 broker_error(
983 "AlterPartitionReassignments",
984 response.error_code,
985 response.error_message,
986 )?;
987 Ok(response
988 .responses
989 .into_iter()
990 .flat_map(|topic| {
991 topic
992 .partitions
993 .into_iter()
994 .map(move |partition| PartitionAssignmentOutcome {
995 topic: topic.name.clone(),
996 partition: partition.partition_index,
997 error: kafka_error_if(partition.error_code, partition.error_message),
998 })
999 })
1000 .collect())
1001}
1002
1003fn build_metadata(topics: &[&str]) -> MetadataRequest {
1004 MetadataRequest {
1005 topics: if topics.is_empty() {
1006 None
1007 } else {
1008 Some(
1009 topics
1010 .iter()
1011 .map(|n| MetadataRequestTopic {
1012 topic_id: ProtoUuid::ZERO,
1013 name: Some((*n).to_owned()),
1014 ..Default::default()
1015 })
1016 .collect(),
1017 )
1018 },
1019 allow_auto_topic_creation: false,
1020 include_cluster_authorized_operations: false,
1021 include_topic_authorized_operations: false,
1022 ..Default::default()
1023 }
1024}
1025
1026fn broker_error(api: &'static str, code: i16, message: Option<String>) -> Result<(), AdminError> {
1027 if code == 0 {
1028 return Ok(());
1029 }
1030 Err(AdminError::Broker {
1031 api,
1032 code,
1033 name: crate::kafka_error_name(code),
1034 message,
1035 })
1036}
1037
1038fn build_replication_factor_reassignment(
1039 metadata: &MetadataResponse,
1040 eligible_brokers: &[i32],
1041 topic_name: &str,
1042 replication_factor: i32,
1043 timeout: Time,
1044) -> Result<Option<AlterPartitionReassignmentsRequest>, AdminError> {
1045 broker_error("Metadata", metadata.error_code, None)?;
1046 let desired = usize::try_from(replication_factor).map_err(|_| {
1047 AdminError::Protocol("replication factor must be a positive integer".into())
1048 })?;
1049 if desired == 0 {
1050 return Err(AdminError::Protocol(
1051 "replication factor must be a positive integer".into(),
1052 ));
1053 }
1054
1055 let mut brokers = eligible_brokers.to_vec();
1056 brokers.sort_unstable();
1057 brokers.dedup();
1058 if desired > brokers.len() {
1059 return Err(AdminError::Broker {
1060 api: "AlterPartitionReassignments",
1061 code: 38,
1062 name: crate::kafka_error_name(38),
1063 message: Some(format!(
1064 "replication factor {replication_factor} exceeds live broker count {}",
1065 brokers.len()
1066 )),
1067 });
1068 }
1069
1070 let topic = metadata
1071 .topics
1072 .iter()
1073 .find(|topic| topic.name.as_deref() == Some(topic_name))
1074 .ok_or_else(|| AdminError::Broker {
1075 api: "Metadata",
1076 code: 3,
1077 name: crate::kafka_error_name(3),
1078 message: Some(format!("topic {topic_name:?} is absent from metadata")),
1079 })?;
1080 broker_error("Metadata", topic.error_code, None)?;
1081
1082 let available = brokers.iter().copied().collect::<HashSet<_>>();
1083 let mut partitions = Vec::new();
1084 for (rotation, partition) in topic.partitions.iter().enumerate() {
1085 broker_error("Metadata", partition.error_code, None)?;
1086 let mut selected = HashSet::with_capacity(desired);
1087 let mut target = partition
1088 .replica_nodes
1089 .iter()
1090 .copied()
1091 .filter(|broker| available.contains(broker) && selected.insert(*broker))
1092 .take(desired)
1093 .collect::<Vec<_>>();
1094 let start = rotation.checked_rem(brokers.len()).unwrap_or(0);
1095 for &broker in brokers.iter().cycle().skip(start).take(brokers.len()) {
1096 if target.len() == desired {
1097 break;
1098 }
1099 if selected.insert(broker) {
1100 target.push(broker);
1101 }
1102 }
1103 if target != partition.replica_nodes {
1104 partitions.push(ReassignablePartition {
1105 partition_index: partition.partition_index,
1106 replicas: Some(target),
1107 ..Default::default()
1108 });
1109 }
1110 }
1111 if partitions.is_empty() {
1112 return Ok(None);
1113 }
1114
1115 Ok(Some(AlterPartitionReassignmentsRequest {
1116 timeout_ms: timeout.millis_i32(),
1117 allow_replication_factor_change: true,
1118 topics: vec![ReassignableTopic {
1119 name: topic_name.to_owned(),
1120 partitions,
1121 ..Default::default()
1122 }],
1123 ..Default::default()
1124 }))
1125}
1126
1127fn build_create_topics(specs: &[CreateTopicSpec], timeout_ms: i32) -> CreateTopicsRequest {
1128 CreateTopicsRequest {
1129 topics: specs
1130 .iter()
1131 .map(|s| CreatableTopic {
1132 name: s.name.clone(),
1133 num_partitions: s.partitions,
1134 replication_factor: i16::try_from(s.replicas).unwrap_or(i16::MAX),
1135 assignments: s
1136 .replica_assignments
1137 .iter()
1138 .map(|(partition_index, broker_ids)| CreatableReplicaAssignment {
1139 partition_index: *partition_index,
1140 broker_ids: broker_ids.clone(),
1141 ..Default::default()
1142 })
1143 .collect(),
1144 configs: s
1145 .configs
1146 .iter()
1147 .map(|(k, v)| CreatableTopicConfig {
1148 name: k.clone(),
1149 value: Some(v.clone()),
1150 ..Default::default()
1151 })
1152 .collect(),
1153 ..Default::default()
1154 })
1155 .collect(),
1156 timeout_ms,
1157 validate_only: false,
1158 ..Default::default()
1159 }
1160}
1161
1162fn build_delete_topics(names: &[String], timeout_ms: i32) -> DeleteTopicsRequest {
1166 DeleteTopicsRequest {
1167 topic_names: names.to_vec(),
1168 topics: names
1169 .iter()
1170 .map(|name| DeleteTopicState {
1171 name: Some(name.clone()),
1172 topic_id: ProtoUuid::ZERO,
1173 ..Default::default()
1174 })
1175 .collect(),
1176 timeout_ms,
1177 ..Default::default()
1178 }
1179}
1180
1181fn build_create_partitions(ops: &[CreatePartitionsOp], timeout_ms: i32) -> CreatePartitionsRequest {
1182 CreatePartitionsRequest {
1183 topics: ops
1184 .iter()
1185 .map(|op| CreatePartitionsTopic {
1186 name: op.name.clone(),
1187 count: op.new_total_count,
1188 assignments: op.assignments.as_ref().map(|assignments| {
1189 assignments
1190 .iter()
1191 .map(|broker_ids| CreatePartitionsAssignment {
1192 broker_ids: broker_ids.clone(),
1193 ..Default::default()
1194 })
1195 .collect()
1196 }),
1197 ..Default::default()
1198 })
1199 .collect(),
1200 timeout_ms,
1201 validate_only: false,
1202 ..Default::default()
1203 }
1204}
1205
1206fn build_delete_records(ops: &[DeleteRecordsOp], timeout: Time) -> DeleteRecordsRequest {
1207 let mut topics = BTreeMap::<String, Vec<DeleteRecordsPartition>>::new();
1208 for op in ops {
1209 topics
1210 .entry(op.topic.clone())
1211 .or_default()
1212 .push(DeleteRecordsPartition {
1213 partition_index: op.partition,
1214 offset: op.offset,
1215 ..Default::default()
1216 });
1217 }
1218
1219 DeleteRecordsRequest {
1220 topics: topics
1221 .into_iter()
1222 .map(|(name, partitions)| DeleteRecordsTopic {
1223 name,
1224 partitions,
1225 ..Default::default()
1226 })
1227 .collect(),
1228 timeout_ms: timeout.millis_i32(),
1229 ..Default::default()
1230 }
1231}
1232
1233fn parse_create_topics(
1234 resp: <CreateTopicsRequest as krabka_protocol::ProtocolRequest>::Response,
1235) -> Vec<CreateTopicOutcome> {
1236 let throttle_time_ms = resp.throttle_time_ms;
1237 resp.topics
1238 .into_iter()
1239 .map(|t| CreateTopicOutcome {
1240 name: t.name,
1241 topic_id: proto_uuid_to_opt(t.topic_id),
1242 throttle_time: quota_throttle_time(t.error_code, throttle_time_ms),
1243 error: kafka_error_if(t.error_code, t.error_message),
1244 })
1245 .collect()
1246}
1247
1248fn parse_delete_topics(
1249 resp: <DeleteTopicsRequest as krabka_protocol::ProtocolRequest>::Response,
1250) -> Vec<DeleteTopicOutcome> {
1251 let throttle_time_ms = resp.throttle_time_ms;
1252 resp.responses
1253 .into_iter()
1254 .map(|t| DeleteTopicOutcome {
1255 name: t.name.unwrap_or_default(),
1256 throttle_time: quota_throttle_time(t.error_code, throttle_time_ms),
1257 error: kafka_error_if(t.error_code, t.error_message),
1258 })
1259 .collect()
1260}
1261
1262fn parse_create_partitions(
1263 resp: <CreatePartitionsRequest as krabka_protocol::ProtocolRequest>::Response,
1264) -> Vec<CreatePartitionsOutcome> {
1265 let throttle_time_ms = resp.throttle_time_ms;
1266 resp.results
1267 .into_iter()
1268 .map(|t| CreatePartitionsOutcome {
1269 name: t.name,
1270 throttle_time: quota_throttle_time(t.error_code, throttle_time_ms),
1271 error: kafka_error_if(t.error_code, t.error_message),
1272 })
1273 .collect()
1274}
1275
1276fn parse_delete_records(
1277 resp: <DeleteRecordsRequest as krabka_protocol::ProtocolRequest>::Response,
1278) -> Vec<DeleteRecordsOutcome> {
1279 resp.topics
1280 .into_iter()
1281 .flat_map(|topic| {
1282 let topic_name = topic.name;
1283 topic
1284 .partitions
1285 .into_iter()
1286 .map(move |partition| DeleteRecordsOutcome {
1287 topic: topic_name.clone(),
1288 partition: partition.partition_index,
1289 error_code: partition.error_code,
1290 low_watermark: partition.low_watermark,
1291 })
1292 })
1293 .collect()
1294}
1295
1296fn delete_records_error_outcome(op: &DeleteRecordsOp, error_code: i16) -> DeleteRecordsOutcome {
1297 DeleteRecordsOutcome {
1298 topic: op.topic.clone(),
1299 partition: op.partition,
1300 error_code,
1301 low_watermark: -1,
1302 }
1303}
1304
1305fn parse_metadata(
1306 resp: <MetadataRequest as krabka_protocol::ProtocolRequest>::Response,
1307) -> TopicMetadata {
1308 let topics = resp
1309 .topics
1310 .into_iter()
1311 .map(|t| {
1312 let partition_count = i32::try_from(t.partitions.len()).unwrap_or(i32::MAX);
1313 let replication_factor = i32::from(t.partitions.first().map_or(0, |p| {
1314 i16::try_from(p.replica_nodes.len()).unwrap_or(i16::MAX)
1315 }));
1316 TopicMetadataEntry {
1317 name: t.name.unwrap_or_default(),
1318 topic_id: proto_uuid_to_opt(t.topic_id),
1319 partition_count,
1320 replication_factor,
1321 error: kafka_error_if(t.error_code, None),
1322 }
1323 })
1324 .collect();
1325 TopicMetadata {
1326 controller_id: resp.controller_id,
1327 topics,
1328 }
1329}
1330
1331fn controller_endpoint(
1332 resp: &<MetadataRequest as krabka_protocol::ProtocolRequest>::Response,
1333) -> Option<String> {
1334 let id = resp.controller_id;
1335 resp.brokers
1336 .iter()
1337 .find(|b| b.node_id == id && !b.host.is_empty() && b.port > 0)
1338 .map(|b| format!("{}:{}", b.host, b.port))
1339}
1340
1341fn controller_requires_bootstrap_fallback(
1342 resp: &<MetadataRequest as krabka_protocol::ProtocolRequest>::Response,
1343) -> bool {
1344 resp.brokers
1345 .iter()
1346 .any(|broker| broker.node_id == resp.controller_id && broker.port <= 0)
1347}
1348
1349fn proto_uuid_to_opt(u: ProtoUuid) -> Option<Uuid> {
1350 if u == ProtoUuid::ZERO {
1351 None
1352 } else {
1353 Some(Uuid::from_bytes(u.0))
1354 }
1355}
1356
1357#[cfg(test)]
1358mod tests {
1359 use std::collections::BTreeMap;
1360
1361 use krabka_protocol::{
1362 UnknownTaggedFields,
1363 owned::metadata_response::{
1364 MetadataResponseBroker, MetadataResponsePartition, MetadataResponseTopic,
1365 },
1366 };
1367
1368 use super::*;
1369 use crate::{AdminClientLike as _, retry::KAFKA_ADMIN_RETRY};
1370
1371 #[test]
1372 fn exact_assignment_request_preserves_replica_order() {
1373 let assignments = maplit::btreemap! {
1374 ("orders".into(), 3) => Some(vec![4, 2, 1]),
1375 };
1376 let request = build_partition_assignment_request(
1377 &assignments,
1378 krabka_units::secs(30),
1379 AlterPartitionReassignmentsOptions::default(),
1380 );
1381 assert2::assert!(
1382 request
1383 == AlterPartitionReassignmentsRequest {
1384 timeout_ms: 30_000,
1385 allow_replication_factor_change: true,
1386 topics: vec![ReassignableTopic {
1387 name: "orders".into(),
1388 partitions: vec![ReassignablePartition {
1389 partition_index: 3,
1390 replicas: Some(vec![4, 2, 1]),
1391 ..Default::default()
1392 }],
1393 ..Default::default()
1394 }],
1395 ..Default::default()
1396 }
1397 );
1398 }
1399
1400 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1405 async fn allow_replication_factor_change_reaches_the_wire_and_needs_v1_when_false() {
1406 use krabka_client_core::{ClientError, MockBroker};
1407 use krabka_protocol::owned::{
1408 alter_partition_reassignments_request,
1409 alter_partition_reassignments_response::AlterPartitionReassignmentsResponse,
1410 api_versions_request,
1411 };
1412
1413 use crate::partition_leaders::test_support::{
1414 ApiVersionRange, api_versions, decode_request, encode_response,
1415 };
1416
1417 for (name, max_version, allow, expected) in [
1420 ("plain call at v1", 1, None, Ok(vec![(1, true)])),
1421 ("default at v1", 1, Some(true), Ok(vec![(1, true)])),
1422 (
1423 "refuse a change at v1",
1424 1,
1425 Some(false),
1426 Ok(vec![(1, false)]),
1427 ),
1428 ("default at v0", 0, Some(true), Ok(vec![(0, true)])),
1429 ("refuse a change at v0", 0, Some(false), Err(())),
1430 ] {
1431 let seen = Arc::new(Mutex::new(Vec::new()));
1432 let requests = Arc::clone(&seen);
1433 let broker = MockBroker::start(move |api_key, version, _, body| match api_key {
1434 api_versions_request::API_KEY => Some(
1435 api_versions()
1436 .apis(vec![
1437 ApiVersionRange::builder()
1438 .api_key(alter_partition_reassignments_request::API_KEY)
1439 .max_version(max_version)
1440 .build(),
1441 ])
1442 .call(),
1443 ),
1444 alter_partition_reassignments_request::API_KEY => {
1445 let request: AlterPartitionReassignmentsRequest =
1446 decode_request(body, version, true);
1447 requests
1448 .lock()
1449 .expect("requests lock")
1450 .push((version, request.allow_replication_factor_change));
1451 Some(encode_response(
1452 &AlterPartitionReassignmentsResponse::default(),
1453 version,
1454 true,
1455 ))
1456 }
1457 _ => None,
1458 })
1459 .await
1460 .unwrap();
1461 let mut admin = AdminClient::connect(&[broker.addr.to_string()])
1462 .await
1463 .expect("admin connects");
1464
1465 let assignments = maplit::btreemap! {
1466 ("orders".into(), 0) => Some(vec![1, 2]),
1467 };
1468 let timeout = krabka_units::secs(5);
1469 let outcome = match allow {
1470 None => {
1471 admin
1472 .alter_partition_assignments(&assignments, timeout)
1473 .await
1474 }
1475 Some(allow) => {
1476 admin
1477 .alter_partition_assignments_with(
1478 &assignments,
1479 timeout,
1480 AlterPartitionReassignmentsOptions {
1481 allow_replication_factor_change: allow,
1482 },
1483 )
1484 .await
1485 }
1486 };
1487 let result = outcome
1488 .map(|_| seen.lock().expect("requests lock").clone())
1489 .map_err(|error| {
1490 assert2::assert!(
1491 matches!(
1492 error,
1493 AdminError::Transport(ClientError::IncompatibleVersion { .. })
1494 ),
1495 "case {name}: {error:?}"
1496 );
1497 });
1498
1499 broker.stop();
1500 assert2::assert!(result == expected, "case {name}");
1501 }
1502 }
1503
1504 fn reassignment_metadata(assignments: &[&[i32]]) -> MetadataResponse {
1505 MetadataResponse {
1506 brokers: (1..=3)
1507 .map(|node_id| MetadataResponseBroker {
1508 node_id,
1509 ..Default::default()
1510 })
1511 .collect(),
1512 topics: vec![MetadataResponseTopic {
1513 name: Some("orders".into()),
1514 partitions: assignments
1515 .iter()
1516 .enumerate()
1517 .map(|(partition_index, replicas)| MetadataResponsePartition {
1518 partition_index: i32::try_from(partition_index)
1519 .expect("test partition index fits i32"),
1520 replica_nodes: replicas.to_vec(),
1521 ..Default::default()
1522 })
1523 .collect(),
1524 ..Default::default()
1525 }],
1526 ..Default::default()
1527 }
1528 }
1529
1530 #[test]
1531 fn replication_factor_reassignment_preserves_and_rotates_replicas() {
1532 let cases = [
1533 (
1534 "increase",
1535 reassignment_metadata(&[&[1], &[2]]),
1536 2,
1537 vec![(0, vec![1, 2]), (1, vec![2, 3])],
1538 ),
1539 (
1540 "decrease",
1541 reassignment_metadata(&[&[1, 2, 3], &[2, 3, 1]]),
1542 2,
1543 vec![(0, vec![1, 2]), (1, vec![2, 3])],
1544 ),
1545 ];
1546
1547 for (case, metadata, replication_factor, expected_partitions) in cases {
1548 let actual = build_replication_factor_reassignment(
1549 &metadata,
1550 &[1, 2, 3],
1551 "orders",
1552 replication_factor,
1553 krabka_units::secs(5),
1554 )
1555 .unwrap();
1556 let expected = Some(AlterPartitionReassignmentsRequest {
1557 timeout_ms: 5_000,
1558 allow_replication_factor_change: true,
1559 topics: vec![ReassignableTopic {
1560 name: "orders".into(),
1561 partitions: expected_partitions
1562 .into_iter()
1563 .map(|(partition_index, replicas)| ReassignablePartition {
1564 partition_index,
1565 replicas: Some(replicas),
1566 ..Default::default()
1567 })
1568 .collect(),
1569 ..Default::default()
1570 }],
1571 ..Default::default()
1572 });
1573 assert2::assert!(actual == expected, "case {case}");
1574 }
1575 }
1576
1577 #[test]
1578 fn replication_factor_reassignment_rejects_factor_above_broker_count() {
1579 let error = build_replication_factor_reassignment(
1580 &reassignment_metadata(&[&[1]]),
1581 &[1, 2, 3],
1582 "orders",
1583 4,
1584 krabka_units::secs(5),
1585 )
1586 .unwrap_err();
1587
1588 assert2::assert!(matches!(
1589 error,
1590 AdminError::Broker {
1591 api: "AlterPartitionReassignments",
1592 code: 38,
1593 name: "INVALID_REPLICATION_FACTOR",
1594 ..
1595 }
1596 ));
1597 }
1598
1599 #[test]
1600 fn replication_factor_reassignment_replaces_fenced_replica() {
1601 let actual = build_replication_factor_reassignment(
1602 &reassignment_metadata(&[&[1, 2]]),
1603 &[1, 3],
1604 "orders",
1605 2,
1606 krabka_units::secs(5),
1607 )
1608 .unwrap()
1609 .expect("fenced replica requires reassignment");
1610
1611 assert2::assert!(actual.topics[0].partitions[0].replicas == Some(vec![1, 3]));
1612 }
1613
1614 #[test]
1615 fn build_create_topics_one_spec() {
1616 let req = build_create_topics(
1617 &[CreateTopicSpec::builder()
1618 .name("foo")
1619 .partitions(3)
1620 .replicas(1)
1621 .configs(maplit::btreemap! {
1622 "retention.ms".to_owned() => "60000".to_owned(),
1623 })
1624 .build()],
1625 5_000,
1626 );
1627 assert2::assert!(
1628 req == CreateTopicsRequest {
1629 topics: vec![CreatableTopic {
1630 name: "foo".to_owned(),
1631 num_partitions: 3,
1632 replication_factor: 1,
1633 assignments: vec![],
1634 configs: vec![CreatableTopicConfig {
1635 name: "retention.ms".to_owned(),
1636 value: Some("60000".to_owned()),
1637 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1638 }],
1639 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1640 }],
1641 timeout_ms: 5_000,
1642 validate_only: false,
1643 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1644 }
1645 );
1646 }
1647
1648 #[test]
1652 fn replica_assignments_reach_the_wire_as_kafka_sends_them() {
1653 let topics = build_create_topics(
1654 &[CreateTopicSpec::builder()
1655 .name("placed")
1656 .partitions(-1)
1657 .replicas(-1)
1658 .replica_assignments(maplit::btreemap! {
1659 1 => vec![3, 1],
1660 0 => vec![1, 2],
1661 })
1662 .build()],
1663 5_000,
1664 );
1665 assert2::assert!(
1666 topics.topics
1667 == vec![CreatableTopic {
1668 name: "placed".into(),
1669 num_partitions: -1,
1670 replication_factor: -1,
1671 assignments: vec![
1672 CreatableReplicaAssignment {
1673 partition_index: 0,
1674 broker_ids: vec![1, 2],
1675 ..Default::default()
1676 },
1677 CreatableReplicaAssignment {
1678 partition_index: 1,
1679 broker_ids: vec![3, 1],
1680 ..Default::default()
1681 },
1682 ],
1683 ..Default::default()
1684 }]
1685 );
1686
1687 for (name, assignments, expected) in [
1688 ("broker placement", None, None),
1689 (
1690 "explicit placement",
1691 Some(vec![vec![2, 3], vec![3, 1]]),
1692 Some(vec![
1693 CreatePartitionsAssignment {
1694 broker_ids: vec![2, 3],
1695 ..Default::default()
1696 },
1697 CreatePartitionsAssignment {
1698 broker_ids: vec![3, 1],
1699 ..Default::default()
1700 },
1701 ]),
1702 ),
1703 ] {
1704 let request = build_create_partitions(
1705 &[CreatePartitionsOp {
1706 name: "orders".into(),
1707 new_total_count: 4,
1708 assignments,
1709 }],
1710 5_000,
1711 );
1712 assert2::assert!(
1713 request.topics
1714 == vec![CreatePartitionsTopic {
1715 name: "orders".into(),
1716 count: 4,
1717 assignments: expected,
1718 ..Default::default()
1719 }],
1720 "case {name}"
1721 );
1722 }
1723 }
1724
1725 #[test]
1726 fn build_delete_records_groups_partitions_by_topic() {
1727 let req = build_delete_records(
1728 &[
1729 DeleteRecordsOp {
1730 topic: "beta".to_owned(),
1731 partition: 1,
1732 offset: -1,
1733 },
1734 DeleteRecordsOp {
1735 topic: "alpha".to_owned(),
1736 partition: 0,
1737 offset: 50,
1738 },
1739 DeleteRecordsOp {
1740 topic: "alpha".to_owned(),
1741 partition: 2,
1742 offset: 75,
1743 },
1744 ],
1745 krabka_units::secs(5),
1746 );
1747
1748 assert2::assert!(
1749 req == DeleteRecordsRequest {
1750 topics: vec![
1751 DeleteRecordsTopic {
1752 name: "alpha".to_owned(),
1753 partitions: vec![
1754 DeleteRecordsPartition {
1755 partition_index: 0,
1756 offset: 50,
1757 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1758 },
1759 DeleteRecordsPartition {
1760 partition_index: 2,
1761 offset: 75,
1762 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1763 },
1764 ],
1765 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1766 },
1767 DeleteRecordsTopic {
1768 name: "beta".to_owned(),
1769 partitions: vec![DeleteRecordsPartition {
1770 partition_index: 1,
1771 offset: -1,
1772 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1773 }],
1774 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1775 },
1776 ],
1777 timeout_ms: 5_000,
1778 unknown_tagged_fields: UnknownTaggedFields(vec![]),
1779 }
1780 );
1781 }
1782
1783 #[test]
1784 fn parse_delete_records_flattens_partition_errors() {
1785 use krabka_protocol::owned::delete_records_response::{
1786 DeleteRecordsPartitionResult, DeleteRecordsResponse, DeleteRecordsTopicResult,
1787 };
1788
1789 let resp = DeleteRecordsResponse {
1790 topics: vec![DeleteRecordsTopicResult {
1791 name: "wal".to_owned(),
1792 partitions: vec![
1793 DeleteRecordsPartitionResult {
1794 partition_index: 0,
1795 low_watermark: 50,
1796 error_code: 0,
1797 ..Default::default()
1798 },
1799 DeleteRecordsPartitionResult {
1800 partition_index: 1,
1801 low_watermark: -1,
1802 error_code: 1,
1803 ..Default::default()
1804 },
1805 ],
1806 ..Default::default()
1807 }],
1808 ..Default::default()
1809 };
1810
1811 let outcomes = parse_delete_records(resp);
1812 assert_eq!(
1813 outcomes,
1814 vec![
1815 DeleteRecordsOutcome {
1816 topic: "wal".to_owned(),
1817 partition: 0,
1818 error_code: 0,
1819 low_watermark: 50,
1820 },
1821 DeleteRecordsOutcome {
1822 topic: "wal".to_owned(),
1823 partition: 1,
1824 error_code: 1,
1825 low_watermark: -1,
1826 },
1827 ]
1828 );
1829 }
1830
1831 #[test]
1843 fn not_controller_codes_match_kafka() {
1844 let cases = [
1845 ((NOT_CONTROLLER, false), true),
1846 ((NOT_CONTROLLER, true), true),
1847 ((NOT_LEADER_OR_FOLLOWER, false), false),
1848 ((NOT_LEADER_OR_FOLLOWER, true), true),
1849 ((36, true), false),
1850 ((THROTTLING_QUOTA_EXCEEDED, false), false),
1851 ((0, false), false),
1852 ];
1853 let actual = cases
1854 .map(|((code, bootstrap), _)| ((code, bootstrap), is_not_controller(code, bootstrap)));
1855 assert2::assert!(actual == cases);
1856 }
1857
1858 #[test]
1867 fn controller_endpoint_picks_broker_with_matching_node_id() {
1868 use metadata_response::{MetadataResponse, MetadataResponseBroker};
1869 let resp = MetadataResponse {
1870 controller_id: 2,
1871 brokers: vec![
1872 MetadataResponseBroker {
1873 node_id: 1,
1874 host: "h1".into(),
1875 port: 9092,
1876 rack: None,
1877 ..Default::default()
1878 },
1879 MetadataResponseBroker {
1880 node_id: 2,
1881 host: "h2".into(),
1882 port: 9093,
1883 rack: None,
1884 ..Default::default()
1885 },
1886 ],
1887 ..Default::default()
1888 };
1889 let addr = controller_endpoint(&resp);
1890 assert2::assert!(addr.as_deref() == Some("h2:9093"));
1891 }
1892
1893 #[test]
1897 fn controller_endpoint_returns_none_when_no_match() {
1898 use metadata_response::{MetadataResponse, MetadataResponseBroker};
1899 let resp = MetadataResponse {
1900 controller_id: 99,
1901 brokers: vec![MetadataResponseBroker {
1902 node_id: 1,
1903 host: "h1".into(),
1904 port: 9092,
1905 rack: None,
1906 ..Default::default()
1907 }],
1908 ..Default::default()
1909 };
1910 assert2::assert!(controller_endpoint(&resp).is_none());
1911 }
1912
1913 #[test]
1914 fn controller_endpoint_rejects_non_dialable_ephemeral_port() {
1915 use metadata_response::{MetadataResponse, MetadataResponseBroker};
1916 let resp = MetadataResponse {
1917 controller_id: 1,
1918 brokers: vec![MetadataResponseBroker {
1919 node_id: 1,
1920 host: "127.0.0.1".into(),
1921 port: 0,
1922 ..Default::default()
1923 }],
1924 ..Default::default()
1925 };
1926
1927 assert2::assert!(controller_endpoint(&resp).is_none());
1928 assert2::assert!(controller_requires_bootstrap_fallback(&resp));
1929 }
1930
1931 #[test]
1940 fn parse_metadata_carries_through_per_topic_errors() {
1941 use metadata_response::{MetadataResponse, MetadataResponseTopic};
1942 let resp = MetadataResponse {
1943 topics: vec![
1944 MetadataResponseTopic {
1945 name: Some("ok-topic".into()),
1946 error_code: 0,
1947 ..Default::default()
1948 },
1949 MetadataResponseTopic {
1950 name: Some("missing".into()),
1951 error_code: 3, ..Default::default()
1953 },
1954 ],
1955 ..Default::default()
1956 };
1957 let md = parse_metadata(resp);
1958 assert2::assert!(
1959 md == TopicMetadata {
1960 controller_id: -1,
1961 topics: vec![
1962 TopicMetadataEntry {
1963 name: "ok-topic".to_owned(),
1964 topic_id: None,
1965 partition_count: 0,
1966 replication_factor: 0,
1967 error: None,
1968 },
1969 TopicMetadataEntry {
1970 name: "missing".to_owned(),
1971 topic_id: None,
1972 partition_count: 0,
1973 replication_factor: 0,
1974 error: Some(KafkaError {
1975 code: 3,
1976 name: "UNKNOWN_TOPIC_OR_PARTITION",
1977 message: None,
1978 }),
1979 },
1980 ],
1981 }
1982 );
1983 }
1984
1985 #[test]
1986 fn parse_metadata_zero_uuid_becomes_none() {
1987 use metadata_response::{MetadataResponse, MetadataResponseTopic};
1988 let resp = MetadataResponse {
1989 topics: vec![MetadataResponseTopic {
1990 name: Some("foo".into()),
1991 topic_id: ProtoUuid::ZERO,
1992 ..Default::default()
1993 }],
1994 ..Default::default()
1995 };
1996 let md = parse_metadata(resp);
1997 assert2::assert!(md.topics[0].topic_id.is_none());
1998 }
1999
2000 #[test]
2001 fn parse_metadata_computes_partition_count_and_replication_factor() {
2002 use metadata_response::{
2003 MetadataResponse, MetadataResponsePartition, MetadataResponseTopic,
2004 };
2005 let part = MetadataResponsePartition {
2006 replica_nodes: vec![1, 2],
2007 ..Default::default()
2008 };
2009 let resp = MetadataResponse {
2010 topics: vec![MetadataResponseTopic {
2011 name: Some("foo".into()),
2012 partitions: vec![part.clone(), part.clone(), part],
2013 ..Default::default()
2014 }],
2015 ..Default::default()
2016 };
2017 let md = parse_metadata(resp);
2018 assert2::assert!(
2019 (
2020 md.topics[0].partition_count,
2021 md.topics[0].replication_factor
2022 ) == (3, 2)
2023 );
2024 }
2025
2026 #[test]
2029 fn parse_create_topics_per_topic_error() {
2030 use krabka_protocol::owned::create_topics_response::{
2031 CreatableTopicResult, CreateTopicsResponse,
2032 };
2033 let resp = CreateTopicsResponse {
2034 topics: vec![
2035 CreatableTopicResult {
2036 name: "ok".into(),
2037 topic_id: ProtoUuid([7; 16]),
2038 error_code: 0,
2039 error_message: None,
2040 ..Default::default()
2041 },
2042 CreatableTopicResult {
2043 name: "dup".into(),
2044 error_code: 36, error_message: Some("already there".into()),
2046 ..Default::default()
2047 },
2048 CreatableTopicResult {
2049 name: "throttled".into(),
2050 error_code: THROTTLING_QUOTA_EXCEEDED,
2051 error_message: None,
2052 ..Default::default()
2053 },
2054 ],
2055 throttle_time_ms: 250,
2056 ..Default::default()
2057 };
2058 let outcomes = parse_create_topics(resp);
2059 assert2::assert!(
2060 outcomes
2061 == vec![
2062 CreateTopicOutcome {
2063 name: "ok".to_owned(),
2064 topic_id: Some(Uuid::from_bytes([7; 16])),
2066 error: None,
2067 throttle_time: None,
2068 },
2069 CreateTopicOutcome {
2070 name: "dup".to_owned(),
2071 topic_id: None,
2072 error: Some(KafkaError {
2073 code: 36,
2074 name: "TOPIC_ALREADY_EXISTS",
2075 message: Some("already there".to_owned()),
2076 }),
2077 throttle_time: None,
2078 },
2079 CreateTopicOutcome {
2080 name: "throttled".to_owned(),
2081 topic_id: None,
2082 error: Some(KafkaError {
2083 code: THROTTLING_QUOTA_EXCEEDED,
2084 name: "THROTTLING_QUOTA_EXCEEDED",
2085 message: None,
2086 }),
2087 throttle_time: Some(Time::from_millis(250)),
2088 },
2089 ]
2090 );
2091 }
2092
2093 #[test]
2096 fn parse_delete_topics_handles_missing_name() {
2097 use krabka_protocol::owned::delete_topics_response::{
2098 DeletableTopicResult, DeleteTopicsResponse,
2099 };
2100 let resp = DeleteTopicsResponse {
2101 responses: vec![
2102 DeletableTopicResult {
2103 name: None,
2104 error_code: 0,
2105 ..Default::default()
2106 },
2107 DeletableTopicResult {
2108 name: Some("named".into()),
2109 error_code: 3,
2110 error_message: Some("nope".into()),
2111 ..Default::default()
2112 },
2113 ],
2114 ..Default::default()
2115 };
2116 let outs = parse_delete_topics(resp);
2117 assert2::assert!(
2118 outs == vec![
2119 DeleteTopicOutcome {
2120 name: String::new(),
2123 error: None,
2124 throttle_time: None,
2125 },
2126 DeleteTopicOutcome {
2127 name: "named".to_owned(),
2128 error: Some(KafkaError {
2129 code: 3,
2130 name: "UNKNOWN_TOPIC_OR_PARTITION",
2131 message: Some("nope".to_owned()),
2132 }),
2133 throttle_time: None,
2134 },
2135 ]
2136 );
2137 }
2138
2139 #[test]
2142 fn parse_create_partitions_per_topic_error() {
2143 use krabka_protocol::owned::create_partitions_response::{
2144 CreatePartitionsResponse, CreatePartitionsTopicResult,
2145 };
2146 let resp = CreatePartitionsResponse {
2147 results: vec![
2148 CreatePartitionsTopicResult {
2149 name: "ok".into(),
2150 error_code: 0,
2151 error_message: None,
2152 ..Default::default()
2153 },
2154 CreatePartitionsTopicResult {
2155 name: "bad".into(),
2156 error_code: 37,
2157 error_message: Some("bad count".into()),
2158 ..Default::default()
2159 },
2160 ],
2161 ..Default::default()
2162 };
2163 let outs = parse_create_partitions(resp);
2164 assert2::assert!(
2165 outs == vec![
2166 CreatePartitionsOutcome {
2167 name: "ok".to_owned(),
2168 error: None,
2169 throttle_time: None,
2170 },
2171 CreatePartitionsOutcome {
2172 name: "bad".to_owned(),
2173 error: Some(KafkaError {
2174 code: 37,
2175 name: "INVALID_PARTITIONS",
2176 message: Some("bad count".to_owned()),
2177 }),
2178 throttle_time: None,
2179 },
2180 ]
2181 );
2182 }
2183
2184 use std::sync::{
2187 Arc, Mutex,
2188 atomic::{AtomicBool, AtomicU16, Ordering},
2189 };
2190
2191 use bytes::{Buf, BytesMut};
2192 use krabka_client_core::{MockBroker, MockReply};
2193 use krabka_protocol::{
2194 Decode, Encode,
2195 owned::{
2196 api_versions_request,
2197 api_versions_response::{ApiVersion, ApiVersionsResponse},
2198 create_partitions_request,
2199 create_partitions_response::{CreatePartitionsResponse, CreatePartitionsTopicResult},
2200 create_topics_request,
2201 create_topics_response::{CreatableTopicResult, CreateTopicsResponse},
2202 delete_topics_request,
2203 delete_topics_response::{DeletableTopicResult, DeleteTopicsResponse},
2204 metadata_request,
2205 },
2206 };
2207
2208 #[derive(Clone, Copy, Debug)]
2210 enum Mutation {
2211 CreateTopics,
2212 DeleteTopics,
2213 CreatePartitions,
2214 }
2215
2216 #[derive(Clone, Debug)]
2220 struct Answer {
2221 codes: Vec<(&'static str, i16)>,
2222 throttle_time_ms: i32,
2223 silent: bool,
2225 closed: bool,
2227 }
2228
2229 fn answer(codes: &[(&'static str, i16)]) -> Answer {
2230 Answer {
2231 codes: codes.to_vec(),
2232 throttle_time_ms: 0,
2233 silent: false,
2234 closed: false,
2235 }
2236 }
2237
2238 fn throttled(codes: &[(&'static str, i16)], throttle_time_ms: i32) -> Answer {
2239 Answer {
2240 codes: codes.to_vec(),
2241 throttle_time_ms,
2242 silent: false,
2243 closed: false,
2244 }
2245 }
2246
2247 fn silent() -> Answer {
2248 Answer {
2249 codes: Vec::new(),
2250 throttle_time_ms: 0,
2251 silent: true,
2252 closed: false,
2253 }
2254 }
2255
2256 fn closed() -> Answer {
2257 Answer {
2258 codes: Vec::new(),
2259 throttle_time_ms: 0,
2260 silent: false,
2261 closed: true,
2262 }
2263 }
2264
2265 type SentRequest = (Vec<String>, i32);
2267
2268 type MutationCase = (
2271 &'static str,
2272 Vec<Answer>,
2273 Duration,
2274 TopicMutationOptions,
2275 Vec<Vec<String>>,
2276 Vec<Observed>,
2277 );
2278
2279 type Observed = (String, i16, bool);
2282
2283 fn body(response: &impl Encode, version: i16, flexible: bool) -> Vec<u8> {
2284 let mut bytes = BytesMut::new();
2285 if flexible {
2286 bytes.extend_from_slice(&[0]);
2287 }
2288 response
2289 .encode(&mut bytes, version)
2290 .expect("response encodes");
2291 bytes.to_vec()
2292 }
2293
2294 fn decode_body<R: for<'de> Decode<'de>>(mut body: &[u8], version: i16, flexible: bool) -> R {
2295 let client_id_len = body.get_i16();
2296 body.advance(usize::try_from(client_id_len).expect("client id length"));
2297 if flexible {
2298 body.advance(1);
2299 }
2300 R::decode(&mut body, version).expect("request decodes")
2301 }
2302
2303 async fn scripted_controller(
2307 script: Vec<Answer>,
2308 requests: Arc<Mutex<Vec<SentRequest>>>,
2309 ) -> MockBroker {
2310 let port = Arc::new(AtomicU16::new(0));
2311 let handler_port = Arc::clone(&port);
2312 let mut next = 0_usize;
2313 let close = Arc::new(AtomicBool::new(false));
2314 let handler_close = Arc::clone(&close);
2315 let mut handler = move |api_key: i16, version: i16, request: &[u8]| {
2316 let api_version = |api_key, max_version| ApiVersion {
2317 api_key,
2318 min_version: 0,
2319 max_version,
2320 ..Default::default()
2321 };
2322 let mut take = |names: Vec<String>, timeout_ms: i32| {
2323 requests
2324 .lock()
2325 .expect("requests lock")
2326 .push((names, timeout_ms));
2327 let entry = script[next.min(script.len() - 1)].clone();
2328 next += 1;
2329 handler_close.store(entry.closed, Ordering::SeqCst);
2330 entry
2331 };
2332 match api_key {
2333 api_versions_request::API_KEY => Some(body(
2334 &ApiVersionsResponse {
2335 api_keys: vec![
2336 api_version(api_versions_request::API_KEY, 0),
2337 api_version(metadata_request::API_KEY, 12),
2338 api_version(create_topics_request::API_KEY, 7),
2339 api_version(delete_topics_request::API_KEY, 6),
2340 api_version(create_partitions_request::API_KEY, 3),
2341 ],
2342 ..Default::default()
2343 },
2344 0,
2345 false,
2346 )),
2347 metadata_request::API_KEY => Some(body(
2348 &MetadataResponse {
2349 controller_id: 1,
2350 brokers: vec![MetadataResponseBroker {
2351 node_id: 1,
2352 host: "127.0.0.1".into(),
2353 port: i32::from(handler_port.load(Ordering::SeqCst)),
2354 ..Default::default()
2355 }],
2356 ..Default::default()
2357 },
2358 version,
2359 version >= metadata_request::FLEXIBLE_MIN,
2360 )),
2361 create_topics_request::API_KEY => {
2362 let flexible = version >= create_topics_request::FLEXIBLE_MIN;
2363 let request: CreateTopicsRequest = decode_body(request, version, flexible);
2364 let entry = take(
2365 request
2366 .topics
2367 .iter()
2368 .map(|topic| topic.name.clone())
2369 .collect(),
2370 request.timeout_ms,
2371 );
2372 if entry.silent {
2373 return None;
2374 }
2375 Some(body(
2376 &CreateTopicsResponse {
2377 throttle_time_ms: entry.throttle_time_ms,
2378 topics: entry
2379 .codes
2380 .iter()
2381 .map(|(name, code)| CreatableTopicResult {
2382 name: (*name).to_owned(),
2383 error_code: *code,
2384 ..Default::default()
2385 })
2386 .collect(),
2387 ..Default::default()
2388 },
2389 version,
2390 flexible,
2391 ))
2392 }
2393 delete_topics_request::API_KEY => {
2394 let flexible = version >= delete_topics_request::FLEXIBLE_MIN;
2395 let request: DeleteTopicsRequest = decode_body(request, version, flexible);
2396 let entry = take(
2397 request
2398 .topics
2399 .iter()
2400 .map(|topic| topic.name.clone().unwrap_or_default())
2401 .collect(),
2402 request.timeout_ms,
2403 );
2404 if entry.silent {
2405 return None;
2406 }
2407 Some(body(
2408 &DeleteTopicsResponse {
2409 throttle_time_ms: entry.throttle_time_ms,
2410 responses: entry
2411 .codes
2412 .iter()
2413 .map(|(name, code)| DeletableTopicResult {
2414 name: Some((*name).to_owned()),
2415 error_code: *code,
2416 ..Default::default()
2417 })
2418 .collect(),
2419 ..Default::default()
2420 },
2421 version,
2422 flexible,
2423 ))
2424 }
2425 create_partitions_request::API_KEY => {
2426 let flexible = version >= create_partitions_request::FLEXIBLE_MIN;
2427 let request: CreatePartitionsRequest = decode_body(request, version, flexible);
2428 let entry = take(
2429 request
2430 .topics
2431 .iter()
2432 .map(|topic| topic.name.clone())
2433 .collect(),
2434 request.timeout_ms,
2435 );
2436 if entry.silent {
2437 return None;
2438 }
2439 Some(body(
2440 &CreatePartitionsResponse {
2441 throttle_time_ms: entry.throttle_time_ms,
2442 results: entry
2443 .codes
2444 .iter()
2445 .map(|(name, code)| CreatePartitionsTopicResult {
2446 name: (*name).to_owned(),
2447 error_code: *code,
2448 ..Default::default()
2449 })
2450 .collect(),
2451 ..Default::default()
2452 },
2453 version,
2454 flexible,
2455 ))
2456 }
2457 _ => None,
2458 }
2459 };
2460 let broker = MockBroker::start_with_replies(move |api_key, version, _, request| {
2461 let reply = handler(api_key, version, request);
2462 if close.swap(false, Ordering::SeqCst) {
2463 return MockReply::Close;
2464 }
2465 reply.map_or(MockReply::Silent, MockReply::Respond)
2466 })
2467 .await
2468 .unwrap();
2469 port.store(broker.addr.port(), Ordering::SeqCst);
2470 broker
2471 }
2472
2473 fn observe<T: TopicMutationOutcome>(outcomes: Vec<T>) -> Vec<Observed> {
2474 outcomes
2475 .into_iter()
2476 .map(|outcome| {
2477 (
2478 outcome.topic().to_owned(),
2479 outcome.error_code(),
2480 !outcome.throttle_duration().is_zero(),
2481 )
2482 })
2483 .collect()
2484 }
2485
2486 async fn run_mutation(
2487 admin: &AdminClient,
2488 mutation: Mutation,
2489 topics: &[&str],
2490 options: TopicMutationOptions,
2491 policy: RetryPolicy,
2492 ) -> Vec<Observed> {
2493 match mutation {
2494 Mutation::CreateTopics => {
2495 let specs = topics
2496 .iter()
2497 .map(|name| {
2498 CreateTopicSpec::builder()
2499 .name(*name)
2500 .partitions(1)
2501 .replicas(1)
2502 .build()
2503 })
2504 .collect::<Vec<_>>();
2505 observe(
2506 admin
2507 .create_topics_with_retry(&specs, options, policy)
2508 .await
2509 .expect("create_topics"),
2510 )
2511 }
2512 Mutation::DeleteTopics => observe(
2513 admin
2514 .delete_topics_with_retry(topics, options, policy)
2515 .await
2516 .expect("delete_topics"),
2517 ),
2518 Mutation::CreatePartitions => {
2519 let ops = topics
2520 .iter()
2521 .map(|name| CreatePartitionsOp {
2522 name: (*name).to_owned(),
2523 new_total_count: 2,
2524 ..Default::default()
2525 })
2526 .collect::<Vec<_>>();
2527 observe(
2528 admin
2529 .create_partitions_with_retry(&ops, options, policy)
2530 .await
2531 .expect("create_partitions"),
2532 )
2533 }
2534 }
2535 }
2536
2537 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2545 async fn topic_mutations_retry_not_controller_and_quota_as_kafka_does() {
2546 const LONG: Duration = Duration::from_secs(5);
2547 const NOW: Duration = Duration::from_millis(500);
2550 let ok = |name: &str| (name.to_owned(), 0, false);
2551 let failed = |name: &str, code| (name.to_owned(), code, false);
2552 let quota = |name: &str| (name.to_owned(), THROTTLING_QUOTA_EXCEEDED, true);
2553 let names = |lists: &[&[&str]]| {
2554 lists
2555 .iter()
2556 .map(|list| {
2557 list.iter()
2558 .map(|name| (*name).to_owned())
2559 .collect::<Vec<_>>()
2560 })
2561 .collect::<Vec<_>>()
2562 };
2563 let retry_quota = TopicMutationOptions::default();
2564 let no_quota_retry = TopicMutationOptions {
2565 retry_on_quota_violation: false,
2566 ..TopicMutationOptions::default()
2567 };
2568 let cases: Vec<MutationCase> = vec![
2569 (
2570 "no error",
2571 vec![answer(&[("a", 0), ("b", 0)])],
2572 LONG,
2573 retry_quota,
2574 names(&[&["a", "b"]]),
2575 vec![ok("a"), ok("b")],
2576 ),
2577 (
2578 "not controller twice, then success",
2579 vec![
2580 answer(&[("a", 41), ("b", 41)]),
2581 answer(&[("a", 41), ("b", 41)]),
2582 answer(&[("a", 0), ("b", 0)]),
2583 ],
2584 LONG,
2585 retry_quota,
2586 names(&[&["a", "b"], &["a", "b"], &["a", "b"]]),
2587 vec![ok("a"), ok("b")],
2588 ),
2589 (
2590 "not controller for one topic sends every topic again",
2591 vec![
2592 answer(&[("a", 0), ("b", 41)]),
2593 answer(&[("a", 0), ("b", 0)]),
2594 ],
2595 LONG,
2596 retry_quota,
2597 names(&[&["a", "b"], &["a", "b"]]),
2598 vec![ok("a"), ok("b")],
2599 ),
2600 (
2601 "quota for one topic sends only that topic again",
2602 vec![answer(&[("a", 0), ("b", 89)]), answer(&[("b", 0)])],
2603 LONG,
2604 retry_quota,
2605 names(&[&["a", "b"], &["b"]]),
2606 vec![ok("a"), ok("b")],
2607 ),
2608 (
2609 "quota with retry off is final",
2610 vec![throttled(&[("a", 89), ("b", 0)], 250)],
2611 LONG,
2612 no_quota_retry,
2613 names(&[&["a", "b"]]),
2614 vec![quota("a"), ok("b")],
2615 ),
2616 (
2617 "not controller past the deadline times out",
2618 vec![answer(&[("a", 41), ("b", 41)])],
2619 NOW,
2620 retry_quota,
2621 names(&[&["a", "b"]]),
2622 vec![failed("a", 7), failed("b", 7)],
2623 ),
2624 (
2625 "quota past the deadline keeps the quota error",
2626 vec![throttled(&[("a", 89), ("b", 36)], 60_000)],
2627 Duration::from_millis(100),
2628 retry_quota,
2629 names(&[&["a", "b"]]),
2630 vec![quota("a"), failed("b", 36)],
2631 ),
2632 (
2633 "a silent controller stops the call at the deadline",
2634 vec![silent()],
2635 Duration::from_millis(300),
2636 retry_quota,
2637 names(&[&["a", "b"]]),
2638 vec![failed("a", 7), failed("b", 7)],
2639 ),
2640 (
2641 "a lost connection is retried after the backoff",
2642 vec![closed(), closed(), closed(), answer(&[("a", 0), ("b", 0)])],
2643 LONG,
2644 retry_quota,
2645 names(&[&["a", "b"], &["a", "b"], &["a", "b"], &["a", "b"]]),
2646 vec![ok("a"), ok("b")],
2647 ),
2648 (
2649 "a topic with no result fails",
2650 vec![answer(&[("a", 0)])],
2651 LONG,
2652 retry_quota,
2653 names(&[&["a", "b"]]),
2654 vec![ok("a"), failed("b", -1)],
2655 ),
2656 ];
2657 for mutation in [
2658 Mutation::CreateTopics,
2659 Mutation::DeleteTopics,
2660 Mutation::CreatePartitions,
2661 ] {
2662 for (name, script, timeout, options, expected_requests, expected) in cases.clone() {
2663 let requests = Arc::new(Mutex::new(Vec::new()));
2664 let controller = scripted_controller(script, Arc::clone(&requests)).await;
2665 let admin = AdminClient::connect(&[controller.addr.to_string()])
2666 .await
2667 .expect("admin connects");
2668 let backoff = if timeout == LONG {
2669 Duration::from_millis(1)
2670 } else {
2671 timeout
2672 };
2673 let policy = RetryPolicy {
2674 timeout,
2675 initial_backoff: backoff,
2676 max_backoff: backoff,
2677 jitter: 0.0,
2678 max_retries: u32::MAX,
2679 };
2680
2681 let started = Instant::now();
2682 let observed = run_mutation(&admin, mutation, &["a", "b"], options, policy).await;
2683 let within_deadline = started.elapsed() < timeout + Duration::from_secs(2);
2684
2685 controller.stop();
2686 let requests = requests.lock().expect("requests lock").clone();
2687 let timeouts_within_deadline = requests.iter().all(|(_, timeout_ms)| {
2688 u128::try_from(*timeout_ms).is_ok_and(|ms| ms <= timeout.as_millis())
2689 });
2690 let sent = requests
2691 .into_iter()
2692 .map(|(names, _)| names)
2693 .collect::<Vec<_>>();
2694 assert2::assert!(
2695 (sent, observed, timeouts_within_deadline, within_deadline)
2696 == (expected_requests, expected, true, true),
2697 "{mutation:?}: {name}"
2698 );
2699 }
2700 }
2701 }
2702
2703 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2706 async fn quota_error_at_the_deadline_carries_the_remaining_throttle_time() {
2707 let requests = Arc::new(Mutex::new(Vec::new()));
2708 let controller =
2709 scripted_controller(vec![throttled(&[("a", 89)], 60_000)], Arc::clone(&requests)).await;
2710 let admin = AdminClient::connect(&[controller.addr.to_string()])
2711 .await
2712 .expect("admin connects");
2713 let policy = RetryPolicy {
2714 timeout: Duration::from_millis(100),
2715 ..KAFKA_ADMIN_RETRY
2716 };
2717
2718 let outcomes = admin
2719 .delete_topics_with_retry(&["a"], TopicMutationOptions::default(), policy)
2720 .await
2721 .expect("delete_topics");
2722
2723 controller.stop();
2724 let [outcome] = outcomes.as_slice() else {
2725 panic!("one outcome expected, got {outcomes:?}");
2726 };
2727 let remaining = outcome.throttle_duration();
2728 assert2::assert!(
2729 (
2730 outcome.error.as_ref().map(|error| error.code),
2731 remaining > Duration::from_secs(59),
2732 remaining < Duration::from_mins(1),
2733 ) == (Some(THROTTLING_QUOTA_EXCEEDED), true, true)
2734 );
2735 }
2736
2737 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2742 async fn admin_client_like_topic_calls_reach_the_controller() {
2743 let requests = Arc::new(Mutex::new(Vec::new()));
2744 let controller =
2745 scripted_controller(vec![answer(&[("a", 0)])], Arc::clone(&requests)).await;
2746 let mut admin = AdminClient::connect(&[controller.addr.to_string()])
2747 .await
2748 .expect("admin connects");
2749 let options = TopicMutationOptions::with_timeout(krabka_units::secs(5));
2750
2751 let deleted = observe(
2752 admin
2753 .delete_topics(&["a"], options)
2754 .await
2755 .expect("delete_topics"),
2756 );
2757 let grown = observe(
2758 admin
2759 .create_partitions(
2760 &[CreatePartitionsOp {
2761 name: "a".to_owned(),
2762 new_total_count: 2,
2763 ..Default::default()
2764 }],
2765 options,
2766 )
2767 .await
2768 .expect("create_partitions"),
2769 );
2770 let no_records = admin
2771 .delete_records(&[], krabka_units::secs(5))
2772 .await
2773 .expect("delete_records of nothing");
2774 let unknown_topic = admin
2775 .delete_records(
2776 &[DeleteRecordsOp {
2777 topic: "missing".to_owned(),
2778 partition: 0,
2779 offset: 10,
2780 }],
2781 krabka_units::secs(5),
2782 )
2783 .await
2784 .expect("delete_records of a missing topic");
2785 let assignments = admin
2786 .describe_partition_assignments(&["missing"])
2787 .await
2788 .expect("describe_partition_assignments");
2789
2790 controller.stop();
2791 let sent = requests
2792 .lock()
2793 .expect("requests lock")
2794 .iter()
2795 .map(|(names, timeout_ms)| (names.clone(), *timeout_ms <= 5_000))
2796 .collect::<Vec<_>>();
2797 assert2::assert!(
2798 (deleted, grown, sent)
2799 == (
2800 vec![("a".to_owned(), 0, false)],
2801 vec![("a".to_owned(), 0, false)],
2802 vec![(vec!["a".to_owned()], true), (vec!["a".to_owned()], true)],
2803 )
2804 );
2805 assert2::assert!(
2806 (no_records, unknown_topic, assignments)
2807 == (
2808 Vec::new(),
2809 vec![DeleteRecordsOutcome {
2810 topic: "missing".to_owned(),
2811 partition: 0,
2812 error_code: 3,
2813 low_watermark: -1,
2814 }],
2815 Vec::new(),
2816 )
2817 );
2818 }
2819
2820 fn assert_send<T: Send>(_: T) {}
2821
2822 #[test]
2824 fn controller_call_futures_are_send() {
2825 let _ = |admin: &mut AdminClient, specs: &[CreateTopicSpec], names: &[&str]| {
2826 assert_send(admin.create_topics(specs, TopicMutationOptions::default()));
2827 assert_send(admin.delete_topics(names, TopicMutationOptions::default()));
2828 assert_send(admin.create_partitions(&[], TopicMutationOptions::default()));
2829 assert_send(admin.unregister_broker(1));
2830 assert_send(admin.update_features(
2831 &BTreeMap::new(),
2832 crate::UpdateFeaturesOptions::builder().build(),
2833 ));
2834 assert_send(admin.alter_partition_assignments(&BTreeMap::new(), krabka_units::secs(1)));
2835 assert_send(admin.reconcile_topic_replication_factor("t", 1, krabka_units::secs(1)));
2836 };
2837 }
2838}