1use std::collections::{BTreeMap, BTreeSet};
4
5use bytes::BufMut;
6use futures_util::future::join_all;
7use krabka_client_core::{
8 Connection, ConnectionOptions, CoordinatorKeyType, build_find_coordinator,
9};
10use krabka_protocol::{
11 Encode, ProtocolError, ProtocolRequest,
12 owned::{
13 describe_producers_request::{
14 DescribeProducersRequest, TopicRequest as DescribeProducersTopicRequest,
15 },
16 describe_producers_response::{
17 DescribeProducersResponse, ProducerState as WireProducerState,
18 },
19 describe_transactions_request::DescribeTransactionsRequest,
20 describe_transactions_response::{DescribeTransactionsResponse, TransactionState},
21 find_coordinator_response::FindCoordinatorResponse,
22 init_producer_id_request::{self, InitProducerIdRequest},
23 init_producer_id_response::InitProducerIdResponse,
24 list_transactions_request::ListTransactionsRequest,
25 list_transactions_response::{
26 ListTransactionsResponse, TransactionState as ListedTransactionState,
27 },
28 metadata_request::{MetadataRequest, MetadataRequestTopic},
29 metadata_response::MetadataResponse,
30 write_txn_markers_request::{
31 WritableTxnMarker, WritableTxnMarkerTopic, WriteTxnMarkersRequest,
32 },
33 write_txn_markers_response::WriteTxnMarkersResponse,
34 },
35 primitives::uuid::Uuid as ProtoUuid,
36};
37use krabka_units::{Time, convert::TimeExt};
38
39#[cfg(test)]
40use crate::AdminClientConfig;
41use crate::{
42 AdminClient, AdminError, KafkaError, format_host_port, kafka_error_name,
43 partition_leaders::{PartitionKey, PartitionResults, complete_results},
44 retry::{CoordinatorRetry, RetryAction, RetryDeadline, RetryPolicy, connection_failure_action},
45};
46
47const COORDINATOR_LOAD_IN_PROGRESS: i16 = 14;
49const COORDINATOR_NOT_AVAILABLE: i16 = 15;
51const NOT_COORDINATOR: i16 = 16;
53const CONCURRENT_TRANSACTIONS: i16 = 51;
55const LEADER_NOT_AVAILABLE: i16 = 5;
57const NOT_LEADER_OR_FOLLOWER: i16 = 6;
60
61fn describe_retry_action(
66 result: Result<TransactionDescription, AdminError>,
67) -> RetryAction<TransactionDescription> {
68 match result {
69 Err(AdminError::Broker {
70 code: COORDINATOR_LOAD_IN_PROGRESS,
71 ..
72 }) => RetryAction::SameCoordinator(result),
73 Err(AdminError::Broker {
74 code: COORDINATOR_NOT_AVAILABLE | NOT_COORDINATOR,
75 ..
76 }) => RetryAction::FindCoordinator(result),
77 result => RetryAction::Done(result),
78 }
79}
80
81fn fence_retry_action(error_code: i16) -> RetryAction<()> {
86 if error_code == 0 {
87 return RetryAction::Done(Ok(()));
88 }
89 let result = Err(AdminError::Broker {
90 api: "InitProducerId",
91 code: error_code,
92 name: kafka_error_name(error_code),
93 message: None,
94 });
95 match error_code {
96 COORDINATOR_LOAD_IN_PROGRESS | CONCURRENT_TRANSACTIONS => {
97 RetryAction::SameCoordinator(result)
98 }
99 COORDINATOR_NOT_AVAILABLE | NOT_COORDINATOR => RetryAction::FindCoordinator(result),
100 _ => RetryAction::Done(result),
101 }
102}
103
104#[derive(Debug, Clone, PartialEq)]
113pub struct TransactionDescription {
114 pub transactional_id: String,
116 pub coordinator_id: i32,
118 pub state: String,
120 pub producer_id: i64,
122 pub producer_epoch: i16,
125 pub timeout: Time,
127 pub start_time_ms: Option<i64>,
132 pub topic_partitions: BTreeSet<(String, i32)>,
136}
137
138struct TransactionCoordinator {
141 node_id: i32,
143 connection: Connection,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct TransactionListing {
150 pub transactional_id: String,
152 pub producer_id: i64,
154 pub state: String,
156}
157
158#[derive(Debug, Clone, Default, PartialEq, bon::Builder)]
162pub struct ListTransactionsFilter {
163 #[builder(default)]
166 pub state_filters: Vec<String>,
167 #[builder(default)]
170 pub producer_id_filters: Vec<i64>,
171 #[builder(with = |millis: i64| Time::from_millis(millis))]
174 pub min_duration: Option<Time>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct ProducerStateInfo {
181 pub producer_id: i64,
183 pub producer_epoch: i32,
185 pub last_sequence: i32,
188 pub last_timestamp_ms: i64,
191 pub coordinator_epoch: i32,
193 pub current_txn_start_offset: Option<i64>,
196}
197
198fn coordinator_address(
201 transactional_id: &str,
202 response: FindCoordinatorResponse,
203) -> Result<(i32, String), AdminError> {
204 if let Some(coordinator) = response
205 .coordinators
206 .into_iter()
207 .find(|coordinator| coordinator.key == transactional_id)
208 {
209 if coordinator.error_code != 0 {
210 return Err(AdminError::Broker {
211 api: "FindCoordinator",
212 code: coordinator.error_code,
213 name: kafka_error_name(coordinator.error_code),
214 message: coordinator.error_message,
215 });
216 }
217 return Ok((
218 coordinator.node_id,
219 format!("{}:{}", coordinator.host, coordinator.port),
220 ));
221 }
222
223 if response.error_code != 0 {
224 return Err(AdminError::Broker {
225 api: "FindCoordinator",
226 code: response.error_code,
227 name: kafka_error_name(response.error_code),
228 message: response.error_message,
229 });
230 }
231 if response.host.is_empty() {
232 return Err(AdminError::Protocol(format!(
233 "FindCoordinator returned no entry for transactional id {transactional_id:?}"
234 )));
235 }
236 Ok((
237 response.node_id,
238 format!("{}:{}", response.host, response.port),
239 ))
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
253struct ReleasedInitProducerId(InitProducerIdRequest);
254
255impl Encode for ReleasedInitProducerId {
256 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
257 self.0.encode(buf, version)
258 }
259
260 fn encoded_len(&self, version: i16) -> usize {
261 self.0.encoded_len(version)
262 }
263}
264
265impl ProtocolRequest for ReleasedInitProducerId {
266 const API_KEY: i16 = init_producer_id_request::API_KEY;
267 const MIN_VERSION: i16 = init_producer_id_request::MIN_VERSION;
268 const MAX_VERSION: i16 = 5;
270 const LATEST_STABLE_VERSION: i16 = Self::MAX_VERSION;
272 const FLEXIBLE_MIN: i16 = init_producer_id_request::FLEXIBLE_MIN;
273 type Response = InitProducerIdResponse;
274}
275
276fn force_terminate_request(
277 transactional_id: &str,
278 transaction_timeout_ms: i32,
279) -> ReleasedInitProducerId {
280 ReleasedInitProducerId(InitProducerIdRequest {
281 transactional_id: Some(transactional_id.to_owned()),
282 transaction_timeout_ms,
283 producer_id: -1,
284 producer_epoch: -1,
285 enable2_pc: false,
286 keep_prepared_txn: false,
287 ..Default::default()
288 })
289}
290
291fn describe_transactions_request(transactional_id: &str) -> DescribeTransactionsRequest {
292 DescribeTransactionsRequest {
293 transactional_ids: vec![transactional_id.to_owned()],
294 ..Default::default()
295 }
296}
297
298fn described_transaction(
302 coordinator_id: i32,
303 state: TransactionState,
304) -> Result<TransactionDescription, AdminError> {
305 if state.error_code != 0 {
306 return Err(AdminError::Broker {
307 api: "DescribeTransactions",
308 code: state.error_code,
309 name: kafka_error_name(state.error_code),
310 message: None,
311 });
312 }
313 Ok(TransactionDescription {
314 transactional_id: state.transactional_id,
315 coordinator_id,
316 state: state.transaction_state,
317 producer_id: state.producer_id,
318 producer_epoch: state.producer_epoch,
319 timeout: Time::from_millis(i64::from(state.transaction_timeout_ms)),
320 start_time_ms: (state.transaction_start_time_ms >= 0)
321 .then_some(state.transaction_start_time_ms),
322 topic_partitions: state
323 .topics
324 .into_iter()
325 .flat_map(|topic| {
326 topic
327 .partitions
328 .into_iter()
329 .map(move |partition| (topic.topic.clone(), partition))
330 })
331 .collect(),
332 })
333}
334
335fn transaction_description(
338 transactional_id: &str,
339 coordinator_id: i32,
340 response: DescribeTransactionsResponse,
341) -> Result<TransactionDescription, AdminError> {
342 let state = response
343 .transaction_states
344 .into_iter()
345 .find(|state| state.transactional_id == transactional_id)
346 .ok_or_else(|| {
347 AdminError::Protocol(format!(
348 "DescribeTransactions returned no state for transactional id {transactional_id:?}"
349 ))
350 })?;
351 described_transaction(coordinator_id, state)
352}
353
354fn build_topic_metadata(topics: &[&str]) -> MetadataRequest {
358 MetadataRequest {
359 topics: if topics.is_empty() {
360 Some(Vec::new())
361 } else {
362 Some(
363 topics
364 .iter()
365 .map(|name| MetadataRequestTopic {
366 topic_id: ProtoUuid::ZERO,
367 name: Some((*name).to_owned()),
368 ..Default::default()
369 })
370 .collect(),
371 )
372 },
373 allow_auto_topic_creation: false,
374 ..Default::default()
375 }
376}
377
378fn partition_leader_address(
381 topic: &str,
382 partition: i32,
383 metadata: &MetadataResponse,
384) -> Result<String, AdminError> {
385 let topic_metadata = metadata
386 .topics
387 .iter()
388 .find(|entry| entry.name.as_deref() == Some(topic))
389 .ok_or_else(|| {
390 AdminError::Protocol(format!("Metadata returned no entry for topic {topic:?}"))
391 })?;
392 if topic_metadata.error_code != 0 {
393 return Err(AdminError::Broker {
394 api: "Metadata",
395 code: topic_metadata.error_code,
396 name: kafka_error_name(topic_metadata.error_code),
397 message: None,
398 });
399 }
400 let partition_metadata = topic_metadata
401 .partitions
402 .iter()
403 .find(|entry| entry.partition_index == partition)
404 .ok_or_else(|| {
405 AdminError::Protocol(format!(
406 "Metadata returned no entry for partition {topic}-{partition}"
407 ))
408 })?;
409 if partition_metadata.error_code != 0 {
410 return Err(AdminError::Broker {
411 api: "Metadata",
412 code: partition_metadata.error_code,
413 name: kafka_error_name(partition_metadata.error_code),
414 message: None,
415 });
416 }
417 metadata
418 .brokers
419 .iter()
420 .find(|broker| broker.node_id == partition_metadata.leader_id)
421 .map(|broker| format_host_port(&broker.host, broker.port))
422 .ok_or_else(|| {
423 AdminError::Protocol(format!(
424 "Metadata named leader {} for {topic}-{partition} but listed no such broker",
425 partition_metadata.leader_id
426 ))
427 })
428}
429
430#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct AbortTransactionSpec {
435 pub topic: String,
437 pub partition: i32,
439 pub producer_id: i64,
441 pub producer_epoch: i16,
443 pub coordinator_epoch: i32,
445}
446
447fn abort_transaction_request(spec: &AbortTransactionSpec) -> WriteTxnMarkersRequest {
449 WriteTxnMarkersRequest {
450 markers: vec![WritableTxnMarker {
451 producer_id: spec.producer_id,
452 producer_epoch: spec.producer_epoch,
453 transaction_result: false,
454 topics: vec![WritableTxnMarkerTopic {
455 name: spec.topic.clone(),
456 partition_indexes: vec![spec.partition],
457 ..Default::default()
458 }],
459 coordinator_epoch: spec.coordinator_epoch,
460 ..Default::default()
461 }],
462 ..Default::default()
463 }
464}
465
466fn abort_transaction_result(
469 spec: &AbortTransactionSpec,
470 response: WriteTxnMarkersResponse,
471) -> Result<(), AdminError> {
472 let marker = response
473 .markers
474 .into_iter()
475 .find(|marker| marker.producer_id == spec.producer_id)
476 .ok_or_else(|| {
477 AdminError::Protocol(format!(
478 "WriteTxnMarkers returned no result for producer id {}",
479 spec.producer_id
480 ))
481 })?;
482 let topic_result = marker
483 .topics
484 .into_iter()
485 .find(|entry| entry.name == spec.topic)
486 .ok_or_else(|| {
487 AdminError::Protocol(format!(
488 "WriteTxnMarkers returned no result for topic {:?}",
489 spec.topic
490 ))
491 })?;
492 let partition_result = topic_result
493 .partitions
494 .into_iter()
495 .find(|entry| entry.partition_index == spec.partition)
496 .ok_or_else(|| {
497 AdminError::Protocol(format!(
498 "WriteTxnMarkers returned no result for partition {}-{}",
499 spec.topic, spec.partition
500 ))
501 })?;
502 if partition_result.error_code != 0 {
503 return Err(AdminError::Broker {
504 api: "WriteTxnMarkers",
505 code: partition_result.error_code,
506 name: kafka_error_name(partition_result.error_code),
507 message: None,
508 });
509 }
510 Ok(())
511}
512
513fn abort_transaction_retry_action(result: Result<(), AdminError>) -> RetryAction<()> {
518 match result {
519 Err(AdminError::Broker {
520 code: NOT_LEADER_OR_FOLLOWER | LEADER_NOT_AVAILABLE,
521 ..
522 }) => RetryAction::FindCoordinator(result),
523 result => RetryAction::Done(result),
524 }
525}
526
527fn describe_producers_request(keys: &[(String, i32)]) -> DescribeProducersRequest {
531 let mut by_topic = BTreeMap::<&str, Vec<i32>>::new();
532 for (topic, partition) in keys {
533 by_topic.entry(topic.as_str()).or_default().push(*partition);
534 }
535 DescribeProducersRequest {
536 topics: by_topic
537 .into_iter()
538 .map(|(name, partition_indexes)| DescribeProducersTopicRequest {
539 name: name.to_owned(),
540 partition_indexes,
541 ..Default::default()
542 })
543 .collect(),
544 ..Default::default()
545 }
546}
547
548fn producer_state_info(state: &WireProducerState) -> ProducerStateInfo {
551 ProducerStateInfo {
552 producer_id: state.producer_id,
553 producer_epoch: state.producer_epoch,
554 last_sequence: state.last_sequence,
555 last_timestamp_ms: state.last_timestamp,
556 coordinator_epoch: state.coordinator_epoch,
557 current_txn_start_offset: (state.current_txn_start_offset != -1)
558 .then_some(state.current_txn_start_offset),
559 }
560}
561
562fn describe_producers_results(
566 keys: &[PartitionKey],
567 response: DescribeProducersResponse,
568) -> PartitionResults<Vec<ProducerStateInfo>> {
569 let mut out = BTreeMap::new();
570 for topic in response.topics {
571 for partition in topic.partitions {
572 let result = if partition.error_code == 0 {
573 Ok(partition
574 .active_producers
575 .iter()
576 .map(producer_state_info)
577 .collect())
578 } else {
579 Err(KafkaError {
580 code: partition.error_code,
581 name: kafka_error_name(partition.error_code),
582 message: partition.error_message,
583 })
584 };
585 out.insert((topic.name.clone(), partition.partition_index), result);
586 }
587 }
588 complete_results("DescribeProducers", keys, out)
589}
590
591const fn describe_producers_retry_code(code: i16) -> bool {
595 code == NOT_LEADER_OR_FOLLOWER
596}
597
598async fn describe_producers_on_leader(
601 address: String,
602 options: ConnectionOptions,
603 keys: Vec<PartitionKey>,
604) -> Result<PartitionResults<Vec<ProducerStateInfo>>, AdminError> {
605 let connection = AdminClient::connect_one(&address, options).await?;
606 let response = connection.send(describe_producers_request(&keys)).await?;
607 Ok(describe_producers_results(&keys, response))
608}
609
610fn list_transactions_request(filter: &ListTransactionsFilter) -> ListTransactionsRequest {
613 ListTransactionsRequest {
614 state_filters: filter.state_filters.clone(),
615 producer_id_filters: filter.producer_id_filters.clone(),
616 duration_filter: filter.min_duration.map_or(-1, TimeExt::millis_i64),
617 ..Default::default()
618 }
619}
620
621fn transaction_listing(state: ListedTransactionState) -> TransactionListing {
623 TransactionListing {
624 transactional_id: state.transactional_id,
625 producer_id: state.producer_id,
626 state: state.transaction_state,
627 }
628}
629
630async fn list_transactions_on_broker(
633 address: String,
634 options: ConnectionOptions,
635 request: ListTransactionsRequest,
636) -> Result<Vec<TransactionListing>, AdminError> {
637 let connection = AdminClient::connect_one(&address, options).await?;
638 let response: ListTransactionsResponse = connection.send(request).await?;
639 if response.error_code != 0 {
640 return Err(AdminError::Broker {
641 api: "ListTransactions",
642 code: response.error_code,
643 name: kafka_error_name(response.error_code),
644 message: None,
645 });
646 }
647 Ok(response
648 .transaction_states
649 .into_iter()
650 .map(transaction_listing)
651 .collect())
652}
653
654impl AdminClient {
655 pub async fn force_terminate_transaction(
691 &self,
692 transactional_id: &str,
693 ) -> Result<(), AdminError> {
694 if transactional_id.is_empty() {
695 return Err(AdminError::Protocol(
696 "transactional id must not be empty".to_owned(),
697 ));
698 }
699
700 self.force_terminate_transaction_with_retry(transactional_id, self.retry)
701 .await
702 }
703
704 async fn force_terminate_transaction_with_retry(
705 &self,
706 transactional_id: &str,
707 retry: RetryPolicy,
708 ) -> Result<(), AdminError> {
709 self.force_terminate_transaction_until(transactional_id, retry.start())
710 .await
711 }
712
713 async fn force_terminate_transaction_until(
716 &self,
717 transactional_id: &str,
718 deadline: RetryDeadline,
719 ) -> Result<(), AdminError> {
720 let mut coordinator = None;
721 let mut retry = CoordinatorRetry::from_deadline(deadline);
722 loop {
723 let action = retry
724 .run(self.force_terminate_attempt(
725 transactional_id,
726 &mut coordinator,
727 retry.find_coordinator(),
728 ))
729 .await;
730 if let Some(result) = retry.next(action).await {
731 return result;
732 }
733 }
734 }
735
736 pub async fn fence_producers(
756 &self,
757 transactional_ids: &[&str],
758 ) -> BTreeMap<String, Result<(), AdminError>> {
759 let deadline = self.retry.start();
760 join_all(transactional_ids.iter().map(|transactional_id| {
761 let transactional_id = (*transactional_id).to_owned();
762 async move {
763 let result = if transactional_id.is_empty() {
764 Err(AdminError::Protocol(
765 "transactional id must not be empty".to_owned(),
766 ))
767 } else {
768 self.force_terminate_transaction_until(&transactional_id, deadline)
769 .await
770 };
771 (transactional_id, result)
772 }
773 }))
774 .await
775 .into_iter()
776 .collect()
777 }
778
779 pub async fn abort_transaction(&self, spec: &AbortTransactionSpec) -> Result<(), AdminError> {
807 if spec.topic.is_empty() {
808 return Err(AdminError::Protocol("topic must not be empty".to_owned()));
809 }
810
811 let mut leader = None;
812 let mut retry = CoordinatorRetry::new(self.retry);
813 loop {
814 let action = retry
815 .run(self.abort_transaction_attempt(spec, &mut leader, retry.find_coordinator()))
816 .await;
817 if let Some(result) = retry.next(action).await {
818 return result;
819 }
820 }
821 }
822
823 async fn abort_transaction_attempt(
825 &self,
826 spec: &AbortTransactionSpec,
827 leader: &mut Option<Connection>,
828 find_leader: bool,
829 ) -> RetryAction<()> {
830 let connection = match self
831 .partition_leader_connection(&spec.topic, spec.partition, leader, find_leader)
832 .await
833 {
834 Ok(connection) => connection,
835 Err(action) => return action,
836 };
837 match connection.send(abort_transaction_request(spec)).await {
838 Ok(response) => {
839 abort_transaction_retry_action(abort_transaction_result(spec, response))
840 }
841 Err(error) => connection_failure_action(error.into()),
842 }
843 }
844
845 async fn partition_leader_connection<'c, T>(
849 &self,
850 topic: &str,
851 partition: i32,
852 leader: &'c mut Option<Connection>,
853 find_leader: bool,
854 ) -> Result<&'c Connection, RetryAction<T>> {
855 if find_leader {
856 *leader = None;
857 }
858 if let Some(connection) = leader {
859 return Ok(connection);
860 }
861 let metadata = self
862 .conn
863 .send(build_topic_metadata(&[topic]))
864 .await
865 .map_err(connection_failure_action)?;
866 let address = match partition_leader_address(topic, partition, &metadata) {
867 Ok(address) => address,
868 Err(
869 error @ AdminError::Broker {
870 code: NOT_LEADER_OR_FOLLOWER | LEADER_NOT_AVAILABLE,
871 ..
872 },
873 ) => return Err(RetryAction::FindCoordinator(Err(error))),
874 Err(error) => return Err(RetryAction::Done(Err(error))),
875 };
876 match Self::connect_one(&address, self.options.clone()).await {
877 Ok(connection) => Ok(leader.insert(connection)),
878 Err(error) => Err(connection_failure_action(error)),
879 }
880 }
881
882 pub async fn describe_producers(
909 &self,
910 partitions: &[(String, i32)],
911 ) -> BTreeMap<(String, i32), Result<Vec<ProducerStateInfo>, KafkaError>> {
912 self.call_partition_leaders(
913 "DescribeProducers",
914 partitions,
915 describe_producers_retry_code,
916 |address, keys| describe_producers_on_leader(address, self.options.clone(), keys),
917 )
918 .await
919 }
920
921 pub async fn list_transactions(
939 &self,
940 filter: &ListTransactionsFilter,
941 ) -> Result<Vec<TransactionListing>, AdminError> {
942 let metadata = self.conn.send(build_topic_metadata(&[])).await?;
943 let request = list_transactions_request(filter);
944 let options = self.options.clone();
945 let answers = join_all(metadata.brokers.into_iter().map(|broker| {
946 list_transactions_on_broker(
947 format_host_port(&broker.host, broker.port),
948 options.clone(),
949 request.clone(),
950 )
951 }))
952 .await;
953
954 let mut listings = Vec::new();
955 for answer in answers {
956 listings.extend(answer?);
957 }
958 Ok(listings)
959 }
960
961 async fn force_terminate_attempt(
963 &self,
964 transactional_id: &str,
965 coordinator: &mut Option<TransactionCoordinator>,
966 find_coordinator: bool,
967 ) -> RetryAction<()> {
968 let coordinator = match self
969 .transaction_coordinator(transactional_id, coordinator, find_coordinator)
970 .await
971 {
972 Ok(coordinator) => coordinator,
973 Err(action) => return action,
974 };
975 let request =
976 force_terminate_request(transactional_id, self.options.request_timeout.millis_i32());
977 match coordinator.connection.send(request).await {
978 Ok(response) => fence_retry_action(response.error_code),
979 Err(error) => connection_failure_action(error.into()),
980 }
981 }
982
983 pub async fn describe_transaction(
1018 &self,
1019 transactional_id: &str,
1020 ) -> Result<TransactionDescription, AdminError> {
1021 if transactional_id.is_empty() {
1022 return Err(AdminError::Protocol(
1023 "transactional id must not be empty".to_owned(),
1024 ));
1025 }
1026
1027 self.describe_transaction_with_retry(transactional_id, self.retry)
1028 .await
1029 }
1030
1031 async fn describe_transaction_with_retry(
1032 &self,
1033 transactional_id: &str,
1034 retry: RetryPolicy,
1035 ) -> Result<TransactionDescription, AdminError> {
1036 self.describe_transaction_until(transactional_id, retry.start())
1037 .await
1038 }
1039
1040 async fn describe_transaction_until(
1043 &self,
1044 transactional_id: &str,
1045 deadline: RetryDeadline,
1046 ) -> Result<TransactionDescription, AdminError> {
1047 let mut coordinator = None;
1048 let mut retry = CoordinatorRetry::from_deadline(deadline);
1049 loop {
1050 let action = retry
1051 .run(self.describe_transaction_attempt(
1052 transactional_id,
1053 &mut coordinator,
1054 retry.find_coordinator(),
1055 ))
1056 .await;
1057 if let Some(result) = retry.next(action).await {
1058 return result;
1059 }
1060 }
1061 }
1062
1063 async fn describe_transaction_attempt(
1065 &self,
1066 transactional_id: &str,
1067 coordinator: &mut Option<TransactionCoordinator>,
1068 find_coordinator: bool,
1069 ) -> RetryAction<TransactionDescription> {
1070 let coordinator = match self
1071 .transaction_coordinator(transactional_id, coordinator, find_coordinator)
1072 .await
1073 {
1074 Ok(coordinator) => coordinator,
1075 Err(action) => return action,
1076 };
1077 match coordinator
1078 .connection
1079 .send(describe_transactions_request(transactional_id))
1080 .await
1081 {
1082 Ok(response) => describe_retry_action(transaction_description(
1083 transactional_id,
1084 coordinator.node_id,
1085 response,
1086 )),
1087 Err(error) => connection_failure_action(error.into()),
1088 }
1089 }
1090
1091 async fn transaction_coordinator<'c, T>(
1101 &self,
1102 transactional_id: &str,
1103 coordinator: &'c mut Option<TransactionCoordinator>,
1104 find_coordinator: bool,
1105 ) -> Result<&'c TransactionCoordinator, RetryAction<T>> {
1106 if find_coordinator {
1107 *coordinator = None;
1108 }
1109 if let Some(coordinator) = coordinator {
1110 return Ok(coordinator);
1111 }
1112 let response = self
1113 .conn
1114 .send(build_find_coordinator(
1115 transactional_id,
1116 CoordinatorKeyType::Transaction,
1117 ))
1118 .await
1119 .map_err(connection_failure_action)?;
1120 let (node_id, address) = match coordinator_address(transactional_id, response) {
1121 Ok(found) => found,
1122 Err(
1123 error @ AdminError::Broker {
1124 code: COORDINATOR_LOAD_IN_PROGRESS | COORDINATOR_NOT_AVAILABLE,
1125 ..
1126 },
1127 ) => return Err(RetryAction::FindCoordinator(Err(error))),
1128 Err(error) => return Err(RetryAction::Done(Err(error))),
1129 };
1130 match Self::connect_one(&address, self.options.clone()).await {
1131 Ok(connection) => Ok(coordinator.insert(TransactionCoordinator {
1132 node_id,
1133 connection,
1134 })),
1135 Err(error) => Err(connection_failure_action(error)),
1136 }
1137 }
1138
1139 pub async fn describe_transactions(
1159 &self,
1160 transactional_ids: &[&str],
1161 ) -> Result<Vec<TransactionDescription>, AdminError> {
1162 let deadline = self.retry.start();
1165 let mut descriptions = Vec::with_capacity(transactional_ids.len());
1166 for transactional_id in transactional_ids {
1167 if transactional_id.is_empty() {
1168 return Err(AdminError::Protocol(
1169 "transactional id must not be empty".to_owned(),
1170 ));
1171 }
1172 descriptions.push(
1173 self.describe_transaction_until(transactional_id, deadline)
1174 .await?,
1175 );
1176 }
1177 Ok(descriptions)
1178 }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use std::{
1184 net::SocketAddr,
1185 sync::{Arc, Mutex},
1186 time::Duration,
1187 };
1188
1189 use assert2::check;
1190 use bytes::{Buf, BytesMut};
1191 use krabka_client_core::{ClientError, MockBroker, MockReply};
1192 use krabka_protocol::{
1193 Decode,
1194 owned::{
1195 api_versions_request,
1196 api_versions_response::{ApiVersion, ApiVersionsResponse},
1197 describe_producers_request,
1198 describe_producers_response::{PartitionResponse, TopicResponse},
1199 describe_transactions_request,
1200 describe_transactions_response::TopicData,
1201 find_coordinator_request,
1202 find_coordinator_response::Coordinator,
1203 list_transactions_request, metadata_request,
1204 metadata_response::{
1205 MetadataResponse, MetadataResponseBroker, MetadataResponsePartition,
1206 MetadataResponseTopic,
1207 },
1208 write_txn_markers_request,
1209 write_txn_markers_response::{
1210 WritableTxnMarkerPartitionResult, WritableTxnMarkerResult,
1211 WritableTxnMarkerTopicResult,
1212 },
1213 },
1214 };
1215 use tokio::{net::TcpListener, time::Instant};
1216
1217 use super::*;
1218 use crate::partition_leaders::test_support::{
1219 ApiVersionRange, Seen, fast_admin as leader_fast_admin,
1220 leader_broker as partition_leader_broker,
1221 };
1222
1223 fn state_row(transactional_id: &str, error_code: i16) -> TransactionState {
1224 TransactionState {
1225 error_code,
1226 transactional_id: transactional_id.to_owned(),
1227 ..Default::default()
1228 }
1229 }
1230
1231 fn fence_payments() -> InitProducerIdRequest {
1234 InitProducerIdRequest {
1235 transactional_id: Some("payments".to_owned()),
1236 transaction_timeout_ms: 30_000,
1237 producer_id: -1,
1238 producer_epoch: -1,
1239 enable2_pc: false,
1240 keep_prepared_txn: false,
1241 ..Default::default()
1242 }
1243 }
1244
1245 #[test]
1246 fn force_termination_request_fences_without_preserving_transaction() {
1247 let request = force_terminate_request("payments", 30_000);
1248
1249 assert2::assert!(request == ReleasedInitProducerId(fence_payments()));
1250 }
1251
1252 fn encode_response(response: &impl Encode, version: i16, flexible: bool) -> Vec<u8> {
1255 let mut bytes = BytesMut::new();
1256 if flexible {
1257 bytes.extend_from_slice(&[0]);
1258 }
1259 response
1260 .encode(&mut bytes, version)
1261 .expect("response encodes");
1262 bytes.to_vec()
1263 }
1264
1265 fn api_versions(init_producer_id_range: (i16, i16)) -> Vec<u8> {
1268 let api_version = |api_key, min_version, max_version| ApiVersion {
1269 api_key,
1270 min_version,
1271 max_version,
1272 ..Default::default()
1273 };
1274 encode_response(
1275 &ApiVersionsResponse {
1276 api_keys: vec![
1277 api_version(api_versions_request::API_KEY, 0, 0),
1278 api_version(find_coordinator_request::API_KEY, 0, 0),
1279 api_version(metadata_request::API_KEY, 13, 13),
1280 api_version(
1281 init_producer_id_request::API_KEY,
1282 init_producer_id_range.0,
1283 init_producer_id_range.1,
1284 ),
1285 ],
1286 ..Default::default()
1287 },
1288 0,
1289 false,
1290 )
1291 }
1292
1293 type FenceResult = Result<(), (i16, i16, i16, i16, i16)>;
1296
1297 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1301 async fn force_terminate_transaction_sends_a_released_init_producer_id_version() {
1302 let sent_request = |version| vec![(version, fence_payments())];
1303 for (name, init_producer_id_range, expected_requests, expected_result) in [
1304 ("coordinator stops at v1", (0, 1), sent_request(1), Ok(())),
1305 ("coordinator stops at v4", (0, 4), sent_request(4), Ok(())),
1306 ("coordinator stops at v5", (0, 5), sent_request(5), Ok(())),
1307 (
1308 "coordinator supports unstable v6",
1309 (0, 6),
1310 sent_request(5),
1311 Ok(()),
1312 ),
1313 (
1314 "coordinator supports only unstable v6",
1315 (6, 6),
1316 Vec::new(),
1317 Err((init_producer_id_request::API_KEY, 6, 6, 0, 5)),
1318 ),
1319 ] {
1320 let requests = Arc::new(Mutex::new(Vec::new()));
1321 let requests_in_mock = Arc::clone(&requests);
1322 let coordinator =
1323 MockBroker::start(move |api_key, version, _, mut body| match api_key {
1324 api_versions_request::API_KEY => Some(api_versions(init_producer_id_range)),
1325 init_producer_id_request::API_KEY => {
1326 let client_id_len = body.get_i16();
1327 body.advance(usize::try_from(client_id_len).expect("client id length"));
1328 let flexible = version >= init_producer_id_request::FLEXIBLE_MIN;
1329 if flexible {
1330 body.advance(1);
1331 }
1332 let request = InitProducerIdRequest::decode(&mut body, version)
1333 .expect("init producer id request decodes");
1334 requests_in_mock
1335 .lock()
1336 .expect("requests lock")
1337 .push((version, request));
1338 Some(encode_response(
1339 &InitProducerIdResponse {
1340 producer_id: 4242,
1341 producer_epoch: 1,
1342 ..Default::default()
1343 },
1344 version,
1345 flexible,
1346 ))
1347 }
1348 _ => None,
1349 })
1350 .await
1351 .unwrap();
1352 let coordinator_addr = coordinator.addr;
1353 let bootstrap = MockBroker::start(move |api_key, version, _, _| match api_key {
1354 api_versions_request::API_KEY => Some(api_versions(init_producer_id_range)),
1355 find_coordinator_request::API_KEY => Some(encode_response(
1356 &FindCoordinatorResponse {
1357 node_id: 2,
1358 host: coordinator_addr.ip().to_string(),
1359 port: i32::from(coordinator_addr.port()),
1360 ..Default::default()
1361 },
1362 version,
1363 false,
1364 )),
1365 metadata_request::API_KEY => {
1366 Some(encode_response(&MetadataResponse::default(), version, true))
1367 }
1368 _ => None,
1369 })
1370 .await
1371 .unwrap();
1372 let admin = AdminClient::connect(&[bootstrap.addr.to_string()])
1373 .await
1374 .expect("admin connects");
1375
1376 let result: FenceResult =
1377 admin
1378 .force_terminate_transaction("payments")
1379 .await
1380 .map_err(|error| match error {
1381 AdminError::Transport(ClientError::IncompatibleVersion {
1382 api_key,
1383 broker_min,
1384 broker_max,
1385 client_min,
1386 client_max,
1387 }) => (api_key, broker_min, broker_max, client_min, client_max),
1388 other => panic!("case {name}: unexpected error {other:?}"),
1389 });
1390
1391 bootstrap.stop();
1392 coordinator.stop();
1393 let requests = requests.lock().expect("requests lock").clone();
1394 assert2::assert!(
1395 (requests, result) == (expected_requests, expected_result),
1396 "case {name}"
1397 );
1398 }
1399 }
1400
1401 #[test]
1404 fn coordinator_lookup_names_the_node_and_address_of_the_key() {
1405 for (name, response) in [
1406 (
1407 "batched entry",
1408 FindCoordinatorResponse {
1409 coordinators: vec![
1410 Coordinator {
1411 key: "other".to_owned(),
1412 node_id: 1,
1413 host: "wrong".to_owned(),
1414 port: 1,
1415 ..Default::default()
1416 },
1417 Coordinator {
1418 key: "payments".to_owned(),
1419 node_id: 3,
1420 host: "coordinator".to_owned(),
1421 port: 9092,
1422 ..Default::default()
1423 },
1424 ],
1425 ..Default::default()
1426 },
1427 ),
1428 (
1429 "top-level fields",
1430 FindCoordinatorResponse {
1431 node_id: 3,
1432 host: "coordinator".to_owned(),
1433 port: 9092,
1434 ..Default::default()
1435 },
1436 ),
1437 ] {
1438 let found = coordinator_address("payments", response).expect("matching coordinator");
1439 assert2::assert!(found == (3, "coordinator:9092".to_owned()), "case {name}");
1440 }
1441 }
1442
1443 #[test]
1444 fn describe_request_asks_for_exactly_one_transactional_id() {
1445 let request = describe_transactions_request("payments");
1446
1447 assert2::assert!(
1448 request
1449 == DescribeTransactionsRequest {
1450 transactional_ids: vec!["payments".to_owned()],
1451 ..Default::default()
1452 }
1453 );
1454 }
1455
1456 #[test]
1460 fn description_maps_the_matching_row_to_the_domain_struct() {
1461 let topic = |topic: &str, partitions: Vec<i32>| TopicData {
1462 topic: topic.to_owned(),
1463 partitions,
1464 ..Default::default()
1465 };
1466 let partitions = |entries: &[(&str, i32)]| {
1467 entries
1468 .iter()
1469 .map(|(topic, partition)| ((*topic).to_owned(), *partition))
1470 .collect::<BTreeSet<_>>()
1471 };
1472 for (name, state, start, topics, expected_start, expected_partitions) in [
1473 (
1474 "ongoing with partitions of two topics",
1475 "Ongoing",
1476 1_700_000_000_123,
1477 vec![topic("orders", vec![1, 0]), topic("audit", vec![2])],
1478 Some(1_700_000_000_123),
1479 partitions(&[("audit", 2), ("orders", 0), ("orders", 1)]),
1480 ),
1481 (
1482 "empty with no start time",
1483 "Empty",
1484 -1,
1485 Vec::new(),
1486 None,
1487 BTreeSet::new(),
1488 ),
1489 (
1490 "start time at the epoch",
1491 "Ongoing",
1492 0,
1493 Vec::new(),
1494 Some(0),
1495 BTreeSet::new(),
1496 ),
1497 ] {
1498 let response = DescribeTransactionsResponse {
1499 transaction_states: vec![
1500 TransactionState {
1501 transactional_id: "other".to_owned(),
1502 producer_id: 11,
1503 producer_epoch: 1,
1504 ..Default::default()
1505 },
1506 TransactionState {
1507 transactional_id: "payments".to_owned(),
1508 transaction_state: state.to_owned(),
1509 transaction_timeout_ms: 60_000,
1510 transaction_start_time_ms: start,
1511 producer_id: 4242,
1512 producer_epoch: 7,
1513 topics,
1514 ..Default::default()
1515 },
1516 ],
1517 ..Default::default()
1518 };
1519
1520 let described = transaction_description("payments", 3, response).expect("matching row");
1521
1522 assert2::assert!(
1523 described
1524 == TransactionDescription {
1525 transactional_id: "payments".to_owned(),
1526 coordinator_id: 3,
1527 state: state.to_owned(),
1528 producer_id: 4242,
1529 producer_epoch: 7,
1530 timeout: Time::from_millis(60_000),
1531 start_time_ms: expected_start,
1532 topic_partitions: expected_partitions,
1533 },
1534 "case {name}"
1535 );
1536 }
1537 }
1538
1539 #[test]
1540 fn description_surfaces_the_error_code_of_the_matching_row() {
1541 for (_case, code, want_name) in [
1542 ("unknown id", 105, "TRANSACTIONAL_ID_NOT_FOUND"),
1543 (
1544 "not authorized",
1545 53,
1546 "TRANSACTIONAL_ID_AUTHORIZATION_FAILED",
1547 ),
1548 ("moved coordinator", 16, "NOT_COORDINATOR"),
1549 ("loading", 14, "COORDINATOR_LOAD_IN_PROGRESS"),
1550 ] {
1551 let response = DescribeTransactionsResponse {
1552 transaction_states: vec![state_row("other", 0), state_row("payments", code)],
1553 ..Default::default()
1554 };
1555
1556 let error = transaction_description("payments", 3, response).expect_err("broker error");
1557 match error {
1558 AdminError::Broker {
1559 api,
1560 code: got,
1561 name,
1562 message,
1563 } => {
1564 check!(
1565 (api, got, name, message)
1566 == ("DescribeTransactions", code, want_name, None)
1567 );
1568 }
1569 other => panic!("expected AdminError::Broker, got {other:?}"),
1570 }
1571 }
1572 }
1573
1574 #[test]
1575 fn description_rejects_a_response_that_omits_the_requested_id() {
1576 let response = DescribeTransactionsResponse {
1577 transaction_states: vec![state_row("other", 0)],
1578 ..Default::default()
1579 };
1580
1581 let error = transaction_description("payments", 3, response).expect_err("missing row");
1582 assert2::assert!(matches!(error, AdminError::Protocol(_)));
1583 }
1584
1585 #[derive(Clone, Copy, Debug)]
1587 enum TransactionCall {
1588 Describe,
1589 ForceTerminate,
1590 }
1591
1592 #[derive(Default)]
1595 struct CoordinatorScript {
1596 find_coordinator: Vec<i16>,
1597 coordinator: Vec<i16>,
1598 find_coordinator_requests: usize,
1599 coordinator_requests: usize,
1600 unreachable_answers: usize,
1603 unreachable: Option<SocketAddr>,
1604 silent: bool,
1606 closed_find_coordinators: usize,
1609 }
1610
1611 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1613 enum CoordinatorBehavior {
1614 Normal,
1616 UnreachableOnce,
1619 Silent,
1621 FindCoordinatorClosedOnce,
1624 }
1625
1626 async fn refused_address() -> SocketAddr {
1628 let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind a port");
1629 listener.local_addr().expect("local address")
1630 }
1631
1632 fn next_code(codes: &[i16], requests: &mut usize) -> i16 {
1633 let code = codes[(*requests).min(codes.len() - 1)];
1634 *requests += 1;
1635 code
1636 }
1637
1638 fn retry_api_versions() -> Vec<u8> {
1640 let api_version = |api_key, min_version, max_version| ApiVersion {
1641 api_key,
1642 min_version,
1643 max_version,
1644 ..Default::default()
1645 };
1646 encode_response(
1647 &ApiVersionsResponse {
1648 api_keys: vec![
1649 api_version(api_versions_request::API_KEY, 0, 0),
1650 api_version(find_coordinator_request::API_KEY, 0, 0),
1651 api_version(metadata_request::API_KEY, 13, 13),
1652 api_version(init_producer_id_request::API_KEY, 0, 0),
1653 api_version(describe_transactions_request::API_KEY, 0, 0),
1654 ],
1655 ..Default::default()
1656 },
1657 0,
1658 false,
1659 )
1660 }
1661
1662 async fn scripted_transaction_broker(
1665 script: Arc<Mutex<CoordinatorScript>>,
1666 coordinator: Arc<Mutex<Option<SocketAddr>>>,
1667 ) -> MockBroker {
1668 MockBroker::start_with_replies(move |api_key, version, _, _| {
1669 let mut guard = script.lock().expect("script lock");
1670 let script = &mut *guard;
1671 if api_key == find_coordinator_request::API_KEY && script.closed_find_coordinators > 0 {
1672 script.closed_find_coordinators -= 1;
1673 script.find_coordinator_requests += 1;
1674 return MockReply::Close;
1675 }
1676 let reply = match api_key {
1677 api_versions_request::API_KEY => Some(retry_api_versions()),
1678 find_coordinator_request::API_KEY => {
1679 let error_code = next_code(
1680 &script.find_coordinator,
1681 &mut script.find_coordinator_requests,
1682 );
1683 let addr = match script.unreachable {
1684 Some(unreachable)
1685 if script.find_coordinator_requests <= script.unreachable_answers =>
1686 {
1687 unreachable
1688 }
1689 _ => coordinator
1690 .lock()
1691 .expect("coordinator lock")
1692 .expect("coordinator address"),
1693 };
1694 Some(encode_response(
1695 &FindCoordinatorResponse {
1696 error_code,
1697 node_id: 2,
1698 host: addr.ip().to_string(),
1699 port: i32::from(addr.port()),
1700 ..Default::default()
1701 },
1702 version,
1703 false,
1704 ))
1705 }
1706 describe_transactions_request::API_KEY => {
1707 let error_code =
1708 next_code(&script.coordinator, &mut script.coordinator_requests);
1709 if script.silent {
1710 return MockReply::Silent;
1711 }
1712 Some(encode_response(
1713 &DescribeTransactionsResponse {
1714 transaction_states: vec![TransactionState {
1715 error_code,
1716 transactional_id: "payments".to_owned(),
1717 transaction_state: "Ongoing".to_owned(),
1718 producer_id: 4242,
1719 producer_epoch: 7,
1720 ..Default::default()
1721 }],
1722 ..Default::default()
1723 },
1724 version,
1725 true,
1726 ))
1727 }
1728 init_producer_id_request::API_KEY => {
1729 let error_code =
1730 next_code(&script.coordinator, &mut script.coordinator_requests);
1731 if script.silent {
1732 return MockReply::Silent;
1733 }
1734 Some(encode_response(
1735 &InitProducerIdResponse {
1736 error_code,
1737 producer_id: 4242,
1738 producer_epoch: 8,
1739 ..Default::default()
1740 },
1741 version,
1742 false,
1743 ))
1744 }
1745 _ => None,
1746 };
1747 drop(guard);
1748 reply.map_or(MockReply::Silent, MockReply::Respond)
1749 })
1750 .await
1751 .unwrap()
1752 }
1753
1754 const TIMED_OUT: i16 = 7;
1757 const LONG: Duration = Duration::from_secs(5);
1759 const NOW: Duration = Duration::from_millis(500);
1762 const SHORT: Duration = Duration::from_millis(300);
1764
1765 type TransactionOutcome = Result<(), (&'static str, i16)>;
1766
1767 type TransactionCase = (
1768 &'static str,
1769 TransactionCall,
1770 Vec<i16>,
1771 Vec<i16>,
1772 (Duration, CoordinatorBehavior),
1773 (TransactionOutcome, usize, usize),
1774 );
1775
1776 const OK: TransactionOutcome = Ok(());
1777
1778 fn failed(api: &'static str, code: i16) -> TransactionOutcome {
1779 Err((api, code))
1780 }
1781
1782 async fn run_transaction_case(case: TransactionCase) {
1785 use TransactionCall::{Describe, ForceTerminate};
1786
1787 let (name, call, find_coordinator, coordinator, (timeout, behavior), expected) = case;
1788 let backoff = if timeout == LONG {
1789 Duration::from_millis(1)
1790 } else {
1791 timeout
1792 };
1793 let script = Arc::new(Mutex::new(CoordinatorScript {
1794 find_coordinator,
1795 coordinator,
1796 unreachable_answers: usize::from(behavior == CoordinatorBehavior::UnreachableOnce),
1797 unreachable: Some(refused_address().await),
1798 silent: behavior == CoordinatorBehavior::Silent,
1799 closed_find_coordinators: usize::from(
1800 behavior == CoordinatorBehavior::FindCoordinatorClosedOnce,
1801 ),
1802 ..CoordinatorScript::default()
1803 }));
1804 let coordinator_addr = Arc::new(Mutex::new(None));
1805 let coordinator =
1806 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
1807 *coordinator_addr.lock().expect("coordinator lock") = Some(coordinator.addr);
1808 let bootstrap =
1809 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
1810 let admin = AdminClient::connect(&[bootstrap.addr.to_string()])
1811 .await
1812 .expect("admin connects");
1813 let retry = RetryPolicy {
1814 timeout,
1815 initial_backoff: backoff,
1816 max_backoff: backoff,
1817 jitter: 0.0,
1818 max_retries: u32::MAX,
1819 };
1820
1821 let started = Instant::now();
1822 let result = match call {
1823 Describe => admin
1824 .describe_transaction_with_retry("payments", retry)
1825 .await
1826 .map(|_| ()),
1827 ForceTerminate => {
1828 admin
1829 .force_terminate_transaction_with_retry("payments", retry)
1830 .await
1831 }
1832 }
1833 .map_err(|error| match error {
1834 AdminError::Broker { api, code, .. } => (api, code),
1835 AdminError::Transport(ClientError::Timeout(_)) => ("timeout", TIMED_OUT),
1836 other => panic!("case {name}: unexpected error {other:?}"),
1837 });
1838
1839 bootstrap.stop();
1840 coordinator.stop();
1841 let script = script.lock().expect("script lock");
1842 let (find_coordinator_requests, coordinator_requests) = (
1843 script.find_coordinator_requests,
1844 script.coordinator_requests,
1845 );
1846 drop(script);
1847 let within_deadline = started.elapsed() < timeout + Duration::from_secs(2);
1849 assert2::assert!(
1850 (
1851 result,
1852 find_coordinator_requests,
1853 coordinator_requests,
1854 within_deadline
1855 ) == (expected.0, expected.1, expected.2, true),
1856 "case {name}"
1857 );
1858 }
1859
1860 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1870 async fn transaction_calls_retry_coordinator_errors_as_kafka_does() {
1871 use TransactionCall::{Describe, ForceTerminate};
1872
1873 for case in [
1874 (
1875 "describe: no error",
1876 Describe,
1877 vec![0],
1878 vec![0],
1879 (LONG, CoordinatorBehavior::Normal),
1880 (OK, 1, 1),
1881 ),
1882 (
1883 "describe: coordinator load in progress retries on the same coordinator",
1884 Describe,
1885 vec![0],
1886 vec![14, 14, 0],
1887 (LONG, CoordinatorBehavior::Normal),
1888 (OK, 1, 3),
1889 ),
1890 (
1891 "describe: not coordinator finds the coordinator again",
1892 Describe,
1893 vec![0],
1894 vec![16, 0],
1895 (LONG, CoordinatorBehavior::Normal),
1896 (OK, 2, 2),
1897 ),
1898 (
1899 "describe: coordinator not available finds the coordinator again",
1900 Describe,
1901 vec![0],
1902 vec![15, 0],
1903 (LONG, CoordinatorBehavior::Normal),
1904 (OK, 2, 2),
1905 ),
1906 (
1907 "describe: concurrent transactions is final",
1908 Describe,
1909 vec![0],
1910 vec![51],
1911 (LONG, CoordinatorBehavior::Normal),
1912 (failed("DescribeTransactions", 51), 1, 1),
1913 ),
1914 (
1915 "describe: transactional id not found is final",
1916 Describe,
1917 vec![0],
1918 vec![105],
1919 (LONG, CoordinatorBehavior::Normal),
1920 (failed("DescribeTransactions", 105), 1, 1),
1921 ),
1922 (
1923 "describe: coordinator load in progress past the timeout times out",
1924 Describe,
1925 vec![0],
1926 vec![14],
1927 (NOW, CoordinatorBehavior::Normal),
1928 (failed("timeout", TIMED_OUT), 1, 1),
1929 ),
1930 (
1931 "describe: find coordinator not available, then the coordinator",
1932 Describe,
1933 vec![15, 0],
1934 vec![0],
1935 (LONG, CoordinatorBehavior::Normal),
1936 (OK, 2, 1),
1937 ),
1938 (
1939 "describe: find coordinator authorization failed is final",
1940 Describe,
1941 vec![53],
1942 vec![0],
1943 (LONG, CoordinatorBehavior::Normal),
1944 (failed("FindCoordinator", 53), 1, 0),
1945 ),
1946 (
1947 "fence: no error",
1948 ForceTerminate,
1949 vec![0],
1950 vec![0],
1951 (LONG, CoordinatorBehavior::Normal),
1952 (OK, 1, 1),
1953 ),
1954 (
1955 "fence: coordinator load in progress retries on the same coordinator",
1956 ForceTerminate,
1957 vec![0],
1958 vec![14, 0],
1959 (LONG, CoordinatorBehavior::Normal),
1960 (OK, 1, 2),
1961 ),
1962 (
1963 "fence: concurrent transactions retries on the same coordinator",
1964 ForceTerminate,
1965 vec![0],
1966 vec![51, 51, 0],
1967 (LONG, CoordinatorBehavior::Normal),
1968 (OK, 1, 3),
1969 ),
1970 (
1971 "fence: not coordinator finds the coordinator again",
1972 ForceTerminate,
1973 vec![0],
1974 vec![16, 0],
1975 (LONG, CoordinatorBehavior::Normal),
1976 (OK, 2, 2),
1977 ),
1978 (
1979 "fence: cluster authorization failed is final",
1980 ForceTerminate,
1981 vec![0],
1982 vec![31],
1983 (LONG, CoordinatorBehavior::Normal),
1984 (failed("InitProducerId", 31), 1, 1),
1985 ),
1986 (
1987 "fence: concurrent transactions past the timeout times out",
1988 ForceTerminate,
1989 vec![0],
1990 vec![51],
1991 (NOW, CoordinatorBehavior::Normal),
1992 (failed("timeout", TIMED_OUT), 1, 1),
1993 ),
1994 (
1995 "fence: find coordinator load in progress, then the coordinator",
1996 ForceTerminate,
1997 vec![14, 0],
1998 vec![0],
1999 (LONG, CoordinatorBehavior::Normal),
2000 (OK, 2, 1),
2001 ),
2002 ] {
2003 run_transaction_case(case).await;
2004 }
2005 }
2006
2007 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2011 async fn transaction_calls_find_the_coordinator_again_after_connection_failures() {
2012 use TransactionCall::{Describe, ForceTerminate};
2013
2014 for case in [
2015 (
2016 "describe: a closed connection during FindCoordinator recovers",
2017 Describe,
2018 vec![0],
2019 vec![0],
2020 (LONG, CoordinatorBehavior::FindCoordinatorClosedOnce),
2021 (OK, 2, 1),
2022 ),
2023 (
2024 "fence: a closed connection during FindCoordinator recovers",
2025 ForceTerminate,
2026 vec![0],
2027 vec![0],
2028 (LONG, CoordinatorBehavior::FindCoordinatorClosedOnce),
2029 (OK, 2, 1),
2030 ),
2031 (
2032 "describe: an unreachable coordinator finds the coordinator again",
2033 Describe,
2034 vec![0],
2035 vec![0],
2036 (LONG, CoordinatorBehavior::UnreachableOnce),
2037 (OK, 2, 1),
2038 ),
2039 (
2040 "describe: a silent coordinator stops the attempt at the deadline",
2041 Describe,
2042 vec![0],
2043 vec![0],
2044 (SHORT, CoordinatorBehavior::Silent),
2045 (failed("timeout", TIMED_OUT), 1, 1),
2046 ),
2047 (
2048 "fence: an unreachable coordinator finds the coordinator again",
2049 ForceTerminate,
2050 vec![0],
2051 vec![0],
2052 (LONG, CoordinatorBehavior::UnreachableOnce),
2053 (OK, 2, 1),
2054 ),
2055 (
2056 "fence: a silent coordinator stops the attempt at the deadline",
2057 ForceTerminate,
2058 vec![0],
2059 vec![0],
2060 (SHORT, CoordinatorBehavior::Silent),
2061 (failed("timeout", TIMED_OUT), 1, 1),
2062 ),
2063 ] {
2064 run_transaction_case(case).await;
2065 }
2066 }
2067
2068 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2071 async fn describe_transaction_names_the_coordinator_that_find_coordinator_gave() {
2072 let script = Arc::new(Mutex::new(CoordinatorScript {
2073 find_coordinator: vec![0],
2074 coordinator: vec![0],
2075 ..CoordinatorScript::default()
2076 }));
2077 let coordinator_addr = Arc::new(Mutex::new(None));
2078 let coordinator =
2079 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2080 *coordinator_addr.lock().expect("coordinator lock") = Some(coordinator.addr);
2081 let bootstrap =
2082 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2083 let admin = AdminClient::connect(&[bootstrap.addr.to_string()])
2084 .await
2085 .expect("admin connects");
2086
2087 let described = admin.describe_transaction("payments").await;
2088
2089 bootstrap.stop();
2090 coordinator.stop();
2091 assert2::assert!(let Ok(described) = described);
2092 assert2::assert!(
2093 described
2094 == TransactionDescription {
2095 transactional_id: "payments".to_owned(),
2096 coordinator_id: 2,
2097 state: "Ongoing".to_owned(),
2098 producer_id: 4242,
2099 producer_epoch: 7,
2100 timeout: Time::from_millis(0),
2101 start_time_ms: Some(0),
2102 topic_partitions: BTreeSet::new(),
2103 }
2104 );
2105 }
2106
2107 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2110 async fn describe_transactions_shares_one_deadline_across_ids() {
2111 let mut coordinator_codes = vec![14; 8];
2112 coordinator_codes.extend([0, 14]);
2113 let script = Arc::new(Mutex::new(CoordinatorScript {
2114 find_coordinator: vec![0],
2115 coordinator: coordinator_codes,
2116 ..CoordinatorScript::default()
2117 }));
2118 let coordinator_addr = Arc::new(Mutex::new(None));
2119 let coordinator =
2120 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2121 *coordinator_addr.lock().expect("coordinator lock") = Some(coordinator.addr);
2122 let bootstrap =
2123 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2124 let admin = AdminClient::connect_with_config(
2125 &[bootstrap.addr.to_string()],
2126 crate::AdminClientConfig {
2127 request_timeout: krabka_units::millis(200),
2128 default_api_timeout: Some(krabka_units::millis(1500)),
2129 retry_backoff: krabka_units::millis(100),
2130 retry_backoff_max: krabka_units::millis(100),
2131 ..AdminClientConfig::default()
2132 },
2133 )
2134 .await
2135 .expect("admin connects");
2136
2137 let started = Instant::now();
2138 let result = admin.describe_transactions(&["payments", "payments"]).await;
2139 let elapsed = started.elapsed();
2140
2141 bootstrap.stop();
2142 coordinator.stop();
2143 assert2::assert!(matches!(
2146 result,
2147 Err(AdminError::Transport(ClientError::Timeout(_)))
2148 ));
2149 assert2::assert!(elapsed < Duration::from_millis(2200), "{elapsed:?}");
2150 }
2151
2152 fn orders_spec() -> AbortTransactionSpec {
2154 AbortTransactionSpec {
2155 topic: "orders".to_owned(),
2156 partition: 1,
2157 producer_id: 4242,
2158 producer_epoch: 7,
2159 coordinator_epoch: 3,
2160 }
2161 }
2162
2163 fn orders_metadata(addr: SocketAddr, partition_error: i16) -> MetadataResponse {
2167 let partition = |partition_index| MetadataResponsePartition {
2168 error_code: partition_error,
2169 partition_index,
2170 leader_id: 1,
2171 ..Default::default()
2172 };
2173 MetadataResponse {
2174 brokers: vec![MetadataResponseBroker {
2175 node_id: 1,
2176 host: addr.ip().to_string(),
2177 port: i32::from(addr.port()),
2178 ..Default::default()
2179 }],
2180 topics: vec![
2181 MetadataResponseTopic {
2182 name: Some("orders".to_owned()),
2183 partitions: vec![partition(0), partition(1)],
2184 ..Default::default()
2185 },
2186 MetadataResponseTopic {
2187 error_code: 3,
2188 name: Some("missing".to_owned()),
2189 ..Default::default()
2190 },
2191 ],
2192 ..Default::default()
2193 }
2194 }
2195
2196 fn outcome_of(result: Result<(), AdminError>) -> TransactionOutcome {
2199 result.map_err(|error| match error {
2200 AdminError::Broker { api, code, .. } => (api, code),
2201 AdminError::Protocol(_) => ("protocol", 0),
2202 other => panic!("unexpected error {other:?}"),
2203 })
2204 }
2205
2206 #[test]
2207 fn partition_leader_address_resolves_the_leader_or_reports_the_metadata_error() {
2208 let addr: SocketAddr = "127.0.0.1:9092".parse().expect("address");
2209 let mut leaderless = orders_metadata(addr, 0);
2210 leaderless.topics[0].partitions[1].leader_id = 9;
2211 for (name, metadata, topic, partition, expected) in [
2212 (
2213 "leader",
2214 orders_metadata(addr, 0),
2215 "orders",
2216 1,
2217 Ok("127.0.0.1:9092".to_owned()),
2218 ),
2219 (
2220 "partition error",
2221 orders_metadata(addr, NOT_LEADER_OR_FOLLOWER),
2222 "orders",
2223 1,
2224 Err(("Metadata", NOT_LEADER_OR_FOLLOWER)),
2225 ),
2226 (
2227 "topic error",
2228 orders_metadata(addr, 0),
2229 "missing",
2230 0,
2231 Err(("Metadata", 3)),
2232 ),
2233 (
2234 "unlisted topic",
2235 orders_metadata(addr, 0),
2236 "absent",
2237 0,
2238 Err(("protocol", 0)),
2239 ),
2240 (
2241 "unlisted partition",
2242 orders_metadata(addr, 0),
2243 "orders",
2244 5,
2245 Err(("protocol", 0)),
2246 ),
2247 (
2248 "unlisted leader",
2249 leaderless,
2250 "orders",
2251 1,
2252 Err(("protocol", 0)),
2253 ),
2254 ] {
2255 let address = partition_leader_address(topic, partition, &metadata);
2256 let actual = match address {
2257 Ok(address) => Ok(address),
2258 Err(error) => Err(outcome_of(Err(error)).expect_err("an error")),
2259 };
2260 assert2::assert!(actual == expected, "case {name}");
2261 }
2262 }
2263
2264 #[test]
2265 fn abort_request_writes_one_abort_marker_for_the_partition() {
2266 assert2::assert!(
2267 abort_transaction_request(&orders_spec())
2268 == WriteTxnMarkersRequest {
2269 markers: vec![WritableTxnMarker {
2270 producer_id: 4242,
2271 producer_epoch: 7,
2272 transaction_result: false,
2273 topics: vec![WritableTxnMarkerTopic {
2274 name: "orders".to_owned(),
2275 partition_indexes: vec![1],
2276 ..Default::default()
2277 }],
2278 coordinator_epoch: 3,
2279 ..Default::default()
2280 }],
2281 ..Default::default()
2282 }
2283 );
2284 }
2285
2286 fn markers_response(
2289 producer_id: i64,
2290 topic: &str,
2291 partition: i32,
2292 code: i16,
2293 ) -> WriteTxnMarkersResponse {
2294 WriteTxnMarkersResponse {
2295 markers: vec![WritableTxnMarkerResult {
2296 producer_id,
2297 topics: vec![WritableTxnMarkerTopicResult {
2298 name: topic.to_owned(),
2299 partitions: vec![WritableTxnMarkerPartitionResult {
2300 partition_index: partition,
2301 error_code: code,
2302 ..Default::default()
2303 }],
2304 ..Default::default()
2305 }],
2306 ..Default::default()
2307 }],
2308 ..Default::default()
2309 }
2310 }
2311
2312 #[test]
2313 fn abort_result_maps_the_matching_partition_result() {
2314 for (name, response, expected) in [
2315 ("no error", markers_response(4242, "orders", 1, 0), OK),
2316 (
2317 "partition error",
2318 markers_response(4242, "orders", 1, 53),
2319 failed("WriteTxnMarkers", 53),
2320 ),
2321 (
2322 "other producer",
2323 markers_response(1, "orders", 1, 0),
2324 failed("protocol", 0),
2325 ),
2326 (
2327 "other topic",
2328 markers_response(4242, "payments", 1, 0),
2329 failed("protocol", 0),
2330 ),
2331 (
2332 "other partition",
2333 markers_response(4242, "orders", 0, 0),
2334 failed("protocol", 0),
2335 ),
2336 ] {
2337 let result = outcome_of(abort_transaction_result(&orders_spec(), response));
2338 assert2::assert!(result == expected, "case {name}");
2339 }
2340 }
2341
2342 #[test]
2346 fn abort_retries_only_leadership_errors() {
2347 #[derive(Debug, PartialEq, Eq)]
2348 enum Action {
2349 Done,
2350 FindLeader,
2351 }
2352 for (code, expected) in [
2353 (0, Action::Done),
2354 (LEADER_NOT_AVAILABLE, Action::FindLeader),
2355 (NOT_LEADER_OR_FOLLOWER, Action::FindLeader),
2356 (53, Action::Done),
2357 (47, Action::Done),
2358 ] {
2359 let result =
2360 abort_transaction_result(&orders_spec(), markers_response(4242, "orders", 1, code));
2361 let action = match abort_transaction_retry_action(result) {
2362 RetryAction::Done(_) => Action::Done,
2363 RetryAction::FindCoordinator(_) => Action::FindLeader,
2364 RetryAction::SameCoordinator(_) => panic!("code {code}: same leader"),
2365 };
2366 assert2::assert!(action == expected, "code {code}");
2367 }
2368 }
2369
2370 #[test]
2371 fn describe_producers_request_groups_partitions_by_topic() {
2372 let keys = [
2373 ("orders".to_owned(), 1),
2374 ("audit".to_owned(), 0),
2375 ("orders".to_owned(), 0),
2376 ];
2377
2378 assert2::assert!(
2379 describe_producers_request(&keys)
2380 == DescribeProducersRequest {
2381 topics: vec![
2382 DescribeProducersTopicRequest {
2383 name: "audit".to_owned(),
2384 partition_indexes: vec![0],
2385 ..Default::default()
2386 },
2387 DescribeProducersTopicRequest {
2388 name: "orders".to_owned(),
2389 partition_indexes: vec![1, 0],
2390 ..Default::default()
2391 },
2392 ],
2393 ..Default::default()
2394 }
2395 );
2396 }
2397
2398 fn orders_producers() -> DescribeProducersResponse {
2402 DescribeProducersResponse {
2403 topics: vec![TopicResponse {
2404 name: "orders".to_owned(),
2405 partitions: vec![
2406 PartitionResponse {
2407 partition_index: 0,
2408 active_producers: vec![
2409 WireProducerState {
2410 producer_id: 4242,
2411 producer_epoch: 7,
2412 last_sequence: 12,
2413 last_timestamp: 1_700_000_000_123,
2414 coordinator_epoch: 3,
2415 current_txn_start_offset: 88,
2416 ..Default::default()
2417 },
2418 WireProducerState {
2419 producer_id: 11,
2420 ..Default::default()
2421 },
2422 ],
2423 ..Default::default()
2424 },
2425 PartitionResponse {
2426 partition_index: 1,
2427 error_code: NOT_LEADER_OR_FOLLOWER,
2428 error_message: Some("moved".to_owned()),
2429 ..Default::default()
2430 },
2431 PartitionResponse {
2432 partition_index: 9,
2433 ..Default::default()
2434 },
2435 ],
2436 ..Default::default()
2437 }],
2438 ..Default::default()
2439 }
2440 }
2441
2442 fn orders_producer_results()
2445 -> BTreeMap<(String, i32), Result<Vec<ProducerStateInfo>, KafkaError>> {
2446 maplit::btreemap! {
2447 ("orders".to_owned(), 0) => Ok(vec![
2448 ProducerStateInfo {
2449 producer_id: 4242,
2450 producer_epoch: 7,
2451 last_sequence: 12,
2452 last_timestamp_ms: 1_700_000_000_123,
2453 coordinator_epoch: 3,
2454 current_txn_start_offset: Some(88),
2455 },
2456 ProducerStateInfo {
2457 producer_id: 11,
2458 producer_epoch: 0,
2459 last_sequence: -1,
2460 last_timestamp_ms: -1,
2461 coordinator_epoch: 0,
2462 current_txn_start_offset: None,
2463 },
2464 ]),
2465 ("orders".to_owned(), 1) => Err(KafkaError {
2466 code: NOT_LEADER_OR_FOLLOWER,
2467 name: "NOT_LEADER_OR_FOLLOWER",
2468 message: Some("moved".to_owned()),
2469 }),
2470 }
2471 }
2472
2473 #[test]
2474 fn describe_producers_results_map_each_requested_partition() {
2475 let keys = [
2476 ("orders".to_owned(), 0),
2477 ("orders".to_owned(), 1),
2478 ("orders".to_owned(), 2),
2479 ];
2480
2481 let mut expected = orders_producer_results();
2482 expected.insert(
2483 ("orders".to_owned(), 2),
2484 Err(KafkaError {
2485 code: -1,
2486 name: "UNKNOWN_SERVER_ERROR",
2487 message: Some(
2488 "the DescribeProducers response did not contain a result for partition \
2489 orders-2"
2490 .to_owned(),
2491 ),
2492 }),
2493 );
2494 assert2::assert!(describe_producers_results(&keys, orders_producers()) == expected);
2495 }
2496
2497 #[test]
2498 fn list_transactions_request_carries_the_filter() {
2499 for (name, filter, expected) in [
2500 (
2501 "default matches everything",
2502 ListTransactionsFilter::builder().build(),
2503 ListTransactionsRequest::default(),
2504 ),
2505 (
2506 "every filter",
2507 ListTransactionsFilter::builder()
2508 .state_filters(vec!["Ongoing".to_owned()])
2509 .producer_id_filters(vec![4242])
2510 .min_duration(90_000)
2511 .build(),
2512 ListTransactionsRequest {
2513 state_filters: vec!["Ongoing".to_owned()],
2514 producer_id_filters: vec![4242],
2515 duration_filter: 90_000,
2516 ..Default::default()
2517 },
2518 ),
2519 ] {
2520 assert2::assert!(
2521 list_transactions_request(&filter) == expected,
2522 "case {name}"
2523 );
2524 }
2525 }
2526
2527 fn decode_request<T: for<'a> Decode<'a>>(mut body: &[u8], version: i16, flexible: bool) -> T {
2530 let client_id_len = body.get_i16();
2531 body.advance(usize::try_from(client_id_len).expect("client id length"));
2532 if flexible {
2533 body.advance(1);
2534 }
2535 T::decode(&mut body, version).expect("request decodes")
2536 }
2537
2538 fn leader_api_versions() -> Vec<u8> {
2540 let api_version = |api_key, min_version, max_version| ApiVersion {
2541 api_key,
2542 min_version,
2543 max_version,
2544 ..Default::default()
2545 };
2546 encode_response(
2547 &ApiVersionsResponse {
2548 api_keys: vec![
2549 api_version(api_versions_request::API_KEY, 0, 0),
2550 api_version(metadata_request::API_KEY, 13, 13),
2551 api_version(write_txn_markers_request::API_KEY, 1, 2),
2552 api_version(list_transactions_request::API_KEY, 0, 2),
2553 ],
2554 ..Default::default()
2555 },
2556 0,
2557 false,
2558 )
2559 }
2560
2561 #[derive(Default)]
2563 struct LeaderScript {
2564 write_codes: Vec<i16>,
2567 write_requests: Vec<WriteTxnMarkersRequest>,
2569 metadata_requests: usize,
2571 list_requests: Vec<ListTransactionsRequest>,
2573 listing: ListTransactionsResponse,
2575 }
2576
2577 async fn leader_broker(
2580 script: Arc<Mutex<LeaderScript>>,
2581 cluster: Arc<Mutex<Vec<SocketAddr>>>,
2582 ) -> MockBroker {
2583 MockBroker::start(move |api_key, version, _, body| {
2584 let mut script = script.lock().expect("script lock");
2585 match api_key {
2586 api_versions_request::API_KEY => Some(leader_api_versions()),
2587 metadata_request::API_KEY => {
2588 script.metadata_requests += 1;
2589 let cluster = cluster.lock().expect("cluster lock").clone();
2590 let mut metadata = orders_metadata(cluster[0], 0);
2591 metadata.brokers = cluster
2592 .iter()
2593 .zip(1..)
2594 .map(|(addr, node_id)| MetadataResponseBroker {
2595 node_id,
2596 host: addr.ip().to_string(),
2597 port: i32::from(addr.port()),
2598 ..Default::default()
2599 })
2600 .collect();
2601 Some(encode_response(&metadata, version, true))
2602 }
2603 write_txn_markers_request::API_KEY => {
2604 let request: WriteTxnMarkersRequest = decode_request(body, version, true);
2605 let code = script.write_codes[script
2606 .write_requests
2607 .len()
2608 .min(script.write_codes.len() - 1)];
2609 script.write_requests.push(request);
2610 Some(encode_response(
2611 &markers_response(4242, "orders", 1, code),
2612 version,
2613 true,
2614 ))
2615 }
2616 list_transactions_request::API_KEY => {
2617 script
2618 .list_requests
2619 .push(decode_request(body, version, true));
2620 Some(encode_response(&script.listing, version, true))
2621 }
2622 _ => None,
2623 }
2624 })
2625 .await
2626 .unwrap()
2627 }
2628
2629 async fn fast_admin(bootstrap: SocketAddr) -> AdminClient {
2631 AdminClient::connect_with_config(
2632 &[bootstrap.to_string()],
2633 crate::AdminClientConfig {
2634 request_timeout: krabka_units::secs(2),
2635 retry_backoff: krabka_units::millis(1),
2636 retry_backoff_max: krabka_units::millis(1),
2637 default_api_timeout: Some(krabka_units::secs(5)),
2638 ..AdminClientConfig::default()
2639 },
2640 )
2641 .await
2642 .expect("admin connects")
2643 }
2644
2645 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2649 async fn abort_transaction_writes_an_abort_marker_to_the_leader() {
2650 for (name, write_codes, expected) in [
2651 ("no error", vec![0], (OK, 1, 1)),
2652 (
2653 "not leader or follower finds the leader again",
2654 vec![NOT_LEADER_OR_FOLLOWER, 0],
2655 (OK, 2, 2),
2656 ),
2657 (
2658 "transactional id authorization failed is final",
2659 vec![53],
2660 (failed("WriteTxnMarkers", 53), 1, 1),
2661 ),
2662 ] {
2663 let script = Arc::new(Mutex::new(LeaderScript {
2664 write_codes,
2665 ..LeaderScript::default()
2666 }));
2667 let cluster = Arc::new(Mutex::new(Vec::new()));
2668 let broker = leader_broker(Arc::clone(&script), Arc::clone(&cluster)).await;
2669 cluster.lock().expect("cluster lock").push(broker.addr);
2670 let admin = fast_admin(broker.addr).await;
2671
2672 let result = outcome_of(admin.abort_transaction(&orders_spec()).await);
2673
2674 broker.stop();
2675 let script = script.lock().expect("script lock");
2676 let (metadata_requests, write_requests) =
2677 (script.metadata_requests, script.write_requests.clone());
2678 drop(script);
2679 let expected_requests = vec![abort_transaction_request(&orders_spec()); expected.2];
2680 assert2::assert!(
2681 (result, metadata_requests, write_requests)
2682 == (expected.0, expected.1, expected_requests),
2683 "case {name}"
2684 );
2685 }
2686 }
2687
2688 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2689 async fn abort_transaction_rejects_an_empty_topic() {
2690 let script = Arc::new(Mutex::new(LeaderScript::default()));
2691 let cluster = Arc::new(Mutex::new(Vec::new()));
2692 let broker = leader_broker(Arc::clone(&script), Arc::clone(&cluster)).await;
2693 cluster.lock().expect("cluster lock").push(broker.addr);
2694 let admin = fast_admin(broker.addr).await;
2695
2696 let result = admin
2697 .abort_transaction(&AbortTransactionSpec {
2698 topic: String::new(),
2699 ..orders_spec()
2700 })
2701 .await;
2702
2703 broker.stop();
2704 assert2::assert!(outcome_of(result) == failed("protocol", 0));
2705 assert2::assert!(script.lock().expect("script lock").metadata_requests == 0);
2706 }
2707
2708 fn producers_answer(
2711 request: &DescribeProducersRequest,
2712 orders_1_code: i16,
2713 ) -> DescribeProducersResponse {
2714 let orders_0 = orders_producers().topics[0].partitions[0].clone();
2715 DescribeProducersResponse {
2716 topics: request
2717 .topics
2718 .iter()
2719 .map(|topic| TopicResponse {
2720 name: topic.name.clone(),
2721 partitions: topic
2722 .partition_indexes
2723 .iter()
2724 .map(|&partition_index| {
2725 if partition_index == 0 {
2726 orders_0.clone()
2727 } else {
2728 PartitionResponse {
2729 partition_index,
2730 error_code: orders_1_code,
2731 error_message: (orders_1_code != 0)
2732 .then(|| "refused".to_owned()),
2733 ..Default::default()
2734 }
2735 }
2736 })
2737 .collect(),
2738 ..Default::default()
2739 })
2740 .collect(),
2741 ..Default::default()
2742 }
2743 }
2744
2745 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2750 async fn describe_producers_finds_the_leader_again_as_kafka_does() {
2751 let both = vec![("orders".to_owned(), 0), ("orders".to_owned(), 1)];
2752 let only_1 = vec![("orders".to_owned(), 1)];
2753 let refused = refused_address().await;
2754 let producers_0 = orders_producer_results()[&both[0]].clone();
2755 for (name, leaders, orders_1_codes, expected) in [
2756 (
2757 "success",
2758 vec![None],
2759 vec![0],
2760 (Ok(Vec::new()), 1, vec![both.clone()]),
2761 ),
2762 (
2763 "not leader or follower finds the leader again",
2764 vec![None],
2765 vec![NOT_LEADER_OR_FOLLOWER, 0],
2766 (Ok(Vec::new()), 2, vec![both.clone(), only_1.clone()]),
2767 ),
2768 (
2769 "a refused connection finds the leader again",
2770 vec![Some(refused), None],
2771 vec![0],
2772 (Ok(Vec::new()), 2, vec![both.clone()]),
2773 ),
2774 (
2775 "topic authorization failed is final",
2776 vec![None],
2777 vec![29],
2778 (
2779 Err(KafkaError {
2780 code: 29,
2781 name: "TOPIC_AUTHORIZATION_FAILED",
2782 message: Some("refused".to_owned()),
2783 }),
2784 1,
2785 vec![both.clone()],
2786 ),
2787 ),
2788 ] {
2789 let seen = Arc::new(Mutex::new(Seen::default()));
2790 let broker = partition_leader_broker(
2791 vec![
2792 ApiVersionRange::builder()
2793 .api_key(describe_producers_request::API_KEY)
2794 .build(),
2795 ],
2796 describe_producers_request::API_KEY,
2797 leaders,
2798 Arc::clone(&seen),
2799 move |n, version, body| {
2800 let request: DescribeProducersRequest = decode_request(body, version, true);
2801 let code = orders_1_codes[n.min(orders_1_codes.len() - 1)];
2802 encode_response(&producers_answer(&request, code), version, true)
2803 },
2804 )
2805 .await;
2806 let admin = leader_fast_admin(broker.addr, krabka_units::secs(5)).await;
2807
2808 let result = admin.describe_producers(&both).await;
2809
2810 broker.stop();
2811 let seen = seen.lock().expect("seen lock").clone();
2812 let requests = seen
2813 .requests
2814 .iter()
2815 .map(|(version, body)| {
2816 decode_request::<DescribeProducersRequest>(body, *version, true)
2817 })
2818 .collect::<Vec<_>>();
2819 assert2::assert!(
2820 (result, seen.metadata_requests, requests)
2821 == (
2822 maplit::btreemap! {
2823 both[0].clone() => producers_0.clone(),
2824 both[1].clone() => expected.0,
2825 },
2826 expected.1,
2827 expected
2828 .2
2829 .iter()
2830 .map(|keys| describe_producers_request(keys))
2831 .collect::<Vec<_>>(),
2832 ),
2833 "case {name}"
2834 );
2835 }
2836 }
2837
2838 fn listing(error_code: i16, transactional_ids: &[&str]) -> ListTransactionsResponse {
2841 ListTransactionsResponse {
2842 error_code,
2843 transaction_states: transactional_ids
2844 .iter()
2845 .zip(1..)
2846 .map(|(transactional_id, producer_id)| ListedTransactionState {
2847 transactional_id: (*transactional_id).to_owned(),
2848 producer_id,
2849 transaction_state: "Ongoing".to_owned(),
2850 ..Default::default()
2851 })
2852 .collect(),
2853 ..Default::default()
2854 }
2855 }
2856
2857 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2861 async fn list_transactions_merges_the_listing_of_every_broker() {
2862 let listed = |transactional_id: &str, producer_id| TransactionListing {
2863 transactional_id: transactional_id.to_owned(),
2864 producer_id,
2865 state: "Ongoing".to_owned(),
2866 };
2867 for (name, second_listing, expected) in [
2868 (
2869 "every broker answers",
2870 listing(0, &["audit"]),
2871 Ok(vec![
2872 listed("payments", 1),
2873 listed("orders", 2),
2874 listed("audit", 1),
2875 ]),
2876 ),
2877 (
2878 "a broker is loading its coordinator",
2879 listing(COORDINATOR_LOAD_IN_PROGRESS, &[]),
2880 Err(("ListTransactions", COORDINATOR_LOAD_IN_PROGRESS)),
2881 ),
2882 ] {
2883 let first = Arc::new(Mutex::new(LeaderScript {
2884 listing: listing(0, &["payments", "orders"]),
2885 ..LeaderScript::default()
2886 }));
2887 let second = Arc::new(Mutex::new(LeaderScript {
2888 listing: second_listing,
2889 ..LeaderScript::default()
2890 }));
2891 let cluster = Arc::new(Mutex::new(Vec::new()));
2892 let first_broker = leader_broker(Arc::clone(&first), Arc::clone(&cluster)).await;
2893 let second_broker = leader_broker(Arc::clone(&second), Arc::clone(&cluster)).await;
2894 cluster
2895 .lock()
2896 .expect("cluster lock")
2897 .extend([first_broker.addr, second_broker.addr]);
2898 let admin = fast_admin(first_broker.addr).await;
2899 let filter = ListTransactionsFilter::builder()
2900 .state_filters(vec!["Ongoing".to_owned()])
2901 .build();
2902
2903 let result = admin
2904 .list_transactions(&filter)
2905 .await
2906 .map_err(|error| match error {
2907 AdminError::Broker { api, code, .. } => (api, code),
2908 other => panic!("case {name}: unexpected error {other:?}"),
2909 });
2910
2911 first_broker.stop();
2912 second_broker.stop();
2913 let requests = [&first, &second]
2914 .map(|script| script.lock().expect("script lock").list_requests.clone());
2915 let expected_request = list_transactions_request(&filter);
2916 assert2::assert!(
2917 (result, requests)
2918 == (
2919 expected,
2920 [vec![expected_request.clone()], vec![expected_request]]
2921 ),
2922 "case {name}"
2923 );
2924 }
2925 }
2926
2927 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2930 async fn fence_producers_gives_each_transactional_id_its_own_result() {
2931 let script = Arc::new(Mutex::new(CoordinatorScript {
2932 find_coordinator: vec![0],
2933 coordinator: vec![0],
2934 ..CoordinatorScript::default()
2935 }));
2936 let coordinator_addr = Arc::new(Mutex::new(None));
2937 let coordinator =
2938 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2939 *coordinator_addr.lock().expect("coordinator lock") = Some(coordinator.addr);
2940 let bootstrap =
2941 scripted_transaction_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2942 let admin = AdminClient::connect(&[bootstrap.addr.to_string()])
2943 .await
2944 .expect("admin connects");
2945
2946 let results = admin.fence_producers(&["payments", "", "orders"]).await;
2947
2948 bootstrap.stop();
2949 coordinator.stop();
2950 let results = results
2951 .into_iter()
2952 .map(|(transactional_id, result)| (transactional_id, outcome_of(result)))
2953 .collect::<BTreeMap<_, _>>();
2954 let script = script.lock().expect("script lock");
2955 let (find_coordinator_requests, coordinator_requests) = (
2956 script.find_coordinator_requests,
2957 script.coordinator_requests,
2958 );
2959 drop(script);
2960 assert2::assert!(
2961 (results, find_coordinator_requests, coordinator_requests)
2962 == (
2963 maplit::btreemap! {
2964 String::new() => failed("protocol", 0),
2965 "orders".to_owned() => OK,
2966 "payments".to_owned() => OK,
2967 },
2968 2,
2969 2
2970 )
2971 );
2972 }
2973
2974 fn assert_send<T: Send>(_: T) {}
2975
2976 #[test]
2979 fn retrying_admin_call_futures_are_send() {
2980 let _ = |admin: &mut AdminClient, offsets: &BTreeMap<(String, i32), i64>| {
2981 assert_send(admin.alter_consumer_group_offsets("workers", offsets));
2982 assert_send(admin.list_consumer_group_offsets("workers"));
2983 assert_send(admin.describe_transaction("payments"));
2984 assert_send(admin.force_terminate_transaction("payments"));
2985 assert_send(admin.fence_producers(&["payments"]));
2986 assert_send(admin.abort_transaction(&orders_spec()));
2987 assert_send(admin.describe_producers(&[("orders".to_owned(), 0)]));
2988 assert_send(admin.list_transactions(&ListTransactionsFilter::builder().build()));
2989 };
2990 }
2991}