Skip to main content

krabka_client_admin/
transactions.rs

1//! Transaction administration.
2
3use 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
47/// `COORDINATOR_LOAD_IN_PROGRESS`: the coordinator is loading its state.
48const COORDINATOR_LOAD_IN_PROGRESS: i16 = 14;
49/// `COORDINATOR_NOT_AVAILABLE`: no broker coordinates the transactional ID now.
50const COORDINATOR_NOT_AVAILABLE: i16 = 15;
51/// `NOT_COORDINATOR`: the broker does not coordinate the transactional ID.
52const NOT_COORDINATOR: i16 = 16;
53/// `CONCURRENT_TRANSACTIONS`: the coordinator is completing a transaction.
54const CONCURRENT_TRANSACTIONS: i16 = 51;
55/// `LEADER_NOT_AVAILABLE`: the metadata has no leader for a partition yet.
56const LEADER_NOT_AVAILABLE: i16 = 5;
57/// `NOT_LEADER_OR_FOLLOWER`: the broker is no longer (or not yet) the leader
58/// of a partition.
59const NOT_LEADER_OR_FOLLOWER: i16 = 6;
60
61/// Map the result of one `DescribeTransactions` attempt to a retry action, as
62/// Kafka's `DescribeTransactionsHandler.handleError` does. 14 retries on the
63/// same coordinator. 15 and 16 unmap the transactional ID, so the next attempt
64/// finds the coordinator again. Every other result is final.
65fn 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
81/// Map the error code of one `InitProducerId` answer to a retry action, as
82/// Kafka's `FenceProducersHandler.handleError` does. 14 and 51 retry on the
83/// same coordinator. 15 and 16 unmap the transactional ID, so the next attempt
84/// finds the coordinator again. Every other code is final.
85fn 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/// The transaction coordinator's view of one transactional ID, as Kafka's
105/// `TransactionDescription`.
106///
107/// A third party reads [`producer_id`](Self::producer_id) and
108/// [`producer_epoch`](Self::producer_epoch) to verify the authority of a
109/// writer. The coordinator raises the epoch each time it fences the previous
110/// producer generation, so a writer that presents an older epoch no longer
111/// holds authority over the transactional ID.
112#[derive(Debug, Clone, PartialEq)]
113pub struct TransactionDescription {
114    /// The transactional ID that the caller asked about.
115    pub transactional_id: String,
116    /// The broker ID of the transaction coordinator that answered.
117    pub coordinator_id: i32,
118    /// The coordinator's state name, such as `Ongoing` or `CompleteCommit`.
119    pub state: String,
120    /// The producer ID that the coordinator holds for this transactional ID.
121    pub producer_id: i64,
122    /// The producer epoch that the coordinator holds for this transactional
123    /// ID.
124    pub producer_epoch: i16,
125    /// The transaction timeout that the producer registered.
126    pub timeout: Time,
127    /// The start of the current transaction, in Kafka epoch milliseconds, or
128    /// `None` when no transaction is open. The coordinator reports `-1` for
129    /// none, and Kafka maps any negative value to an empty `OptionalLong`. An
130    /// instant is a coordinate, so it stays a raw integer.
131    pub start_time_ms: Option<i64>,
132    /// The partitions of the current transaction. While the transaction
133    /// prepares to commit or abort, only the partitions that have no marker
134    /// yet.
135    pub topic_partitions: BTreeSet<(String, i32)>,
136}
137
138/// An open connection to the transaction coordinator of one transactional
139/// ID.
140struct TransactionCoordinator {
141    /// The broker ID that `FindCoordinator` named.
142    node_id: i32,
143    connection: Connection,
144}
145
146/// One transaction that `list_transactions` found on a broker, as Kafka's
147/// `TransactionListing`.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct TransactionListing {
150    /// The transactional ID.
151    pub transactional_id: String,
152    /// The producer ID currently bound to the transactional ID.
153    pub producer_id: i64,
154    /// The coordinator's state name, such as `Ongoing` or `CompleteCommit`.
155    pub state: String,
156}
157
158/// Narrows a [`AdminClient::list_transactions`] call, as Kafka's
159/// `ListTransactionsOptions` does. The default matches every transaction, as
160/// Kafka's default options do.
161#[derive(Debug, Clone, Default, PartialEq, bon::Builder)]
162pub struct ListTransactionsFilter {
163    /// Only transactions whose state name is in this list. Empty matches
164    /// every state.
165    #[builder(default)]
166    pub state_filters: Vec<String>,
167    /// Only transactions whose producer ID is in this list. Empty matches
168    /// every producer ID.
169    #[builder(default)]
170    pub producer_id_filters: Vec<i64>,
171    /// Only transactions that have run for at least this long. `None`
172    /// matches every duration, as Kafka's `-1` filter does.
173    #[builder(with = |millis: i64| Time::from_millis(millis))]
174    pub min_duration: Option<Time>,
175}
176
177/// One producer's state on one partition, as Kafka's `ProducerState`
178/// (`DescribeProducers`, KIP-664).
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct ProducerStateInfo {
181    /// The producer ID.
182    pub producer_id: i64,
183    /// The producer epoch.
184    pub producer_epoch: i32,
185    /// The last sequence number the producer wrote, or `-1` when it has
186    /// written nothing yet.
187    pub last_sequence: i32,
188    /// The timestamp of the last write, in Kafka epoch milliseconds. An
189    /// instant is a coordinate, so it stays a raw integer.
190    pub last_timestamp_ms: i64,
191    /// The epoch of the coordinator that last fenced this producer.
192    pub coordinator_epoch: i32,
193    /// The start offset of the producer's open transaction on this
194    /// partition, or `None` when it has no open transaction.
195    pub current_txn_start_offset: Option<i64>,
196}
197
198/// The broker ID and the address of the coordinator that a
199/// `FindCoordinator` answer names for `transactional_id`.
200fn 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/// An `InitProducerId` request limited to the released versions, v0 to v5.
243///
244/// `InitProducerIdRequest.json` marks v6 as its unstable latest version
245/// (`"latestVersionUnstable": true`, KIP-939), and krabka-protocol's
246/// generated `MAX_VERSION` includes v6. Apache Kafka's
247/// `FenceProducersHandler.buildSingleRequest` uses
248/// `new InitProducerIdRequest.Builder(data)`. That builder calls
249/// `AbstractRequest.Builder(ApiKeys.INIT_PRODUCER_ID)`, which allows
250/// `apiKey.latestVersion(false)`, the latest released version. This type
251/// gives the same cap to version negotiation.
252#[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    /// The latest released `InitProducerId` version.
269    const MAX_VERSION: i16 = 5;
270    /// The cap is a released version, so it is also the stable maximum.
271    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
298/// Converts one row of coordinator `coordinator_id` into the domain struct,
299/// as Kafka's `DescribeTransactionsHandler.handleResponse` does, or surfaces
300/// the error code that the coordinator attached to that row.
301fn 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
335/// Picks the row for `transactional_id` out of the `DescribeTransactions`
336/// response of coordinator `coordinator_id` and maps it to the domain struct.
337fn 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
354/// A `Metadata` request for `topics`, as Kafka's leader-routed handlers send
355/// it. An empty slice asks for the broker list only, with no topics, as
356/// `list_transactions` uses it.
357fn 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
378/// The `host:port` of the leader of `topic`-`partition` in `metadata`, as
379/// Kafka's `PartitionLeaderStrategy` resolves it.
380fn 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/// The identity of a transaction to abort, as Kafka's `AbortTransactionSpec`
431/// bundles it: the partition it last wrote to, and the producer generation
432/// that owns it.
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct AbortTransactionSpec {
435    /// The topic that the transaction wrote to.
436    pub topic: String,
437    /// The partition that the transaction wrote to.
438    pub partition: i32,
439    /// The producer ID of the transaction to fence.
440    pub producer_id: i64,
441    /// The producer epoch of the transaction to fence.
442    pub producer_epoch: i16,
443    /// The epoch of the coordinator that assigned `producer_id`.
444    pub coordinator_epoch: i32,
445}
446
447/// The `WriteTxnMarkers` request that aborts the transaction of `spec`.
448fn 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
466/// Picks the result of `spec`'s topic and partition out of a `WriteTxnMarkers`
467/// response and maps it to an [`AdminError`].
468fn 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
513/// Maps the outcome of one `WriteTxnMarkers` attempt to a retry action.
514/// `NOT_LEADER_OR_FOLLOWER` and `LEADER_NOT_AVAILABLE` look the leader up
515/// again, as Kafka's `AbortTransactionHandler.handleResponse` does. Every
516/// other outcome is final.
517fn 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
527/// The `DescribeProducers` request that asks for every partition in `keys`,
528/// grouped by topic, as Kafka's `DescribeProducersHandler.buildBatchedRequest`
529/// does.
530fn 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
548/// Converts one wire `ProducerState` row into the domain struct. `-1` marks
549/// no open transaction, as Kafka's `ProducerState` does.
550fn 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
562/// Maps a `DescribeProducers` response to a result for each of `keys`. A
563/// partition that the response omits gets `UNKNOWN_SERVER_ERROR`, as Kafka's
564/// `DescribeProducersHandler.handleResponse` does for a missing result.
565fn 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
591/// Whether a `DescribeProducers` partition code sends the partition back to
592/// the leader lookup. Kafka's `DescribeProducersHandler.handlePartitionError`
593/// unmaps the partition on `NOT_LEADER_OR_FOLLOWER` only.
594const fn describe_producers_retry_code(code: i16) -> bool {
595    code == NOT_LEADER_OR_FOLLOWER
596}
597
598/// Sends one `DescribeProducers` request for `keys` to the leader at
599/// `address`.
600async 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
610/// The `ListTransactions` request of `filter`, as Kafka's
611/// `ListTransactionsHandler.buildRequest` builds it for every broker.
612fn 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
621/// Converts one wire `TransactionState` row into the domain struct.
622fn 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
630/// Sends `request` to one broker and returns its transaction listings, or the
631/// broker's top-level error.
632async 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    /// Fences the current producer generation and aborts any ongoing
656    /// transaction for `transactional_id`.
657    ///
658    /// This is Kafka's `forceTerminateTransaction` operation: it discovers the
659    /// transaction coordinator and sends `InitProducerId` with no producer
660    /// identity and `keepPreparedTxn=false`. It is safe to call when no
661    /// transaction is open; the coordinator still advances the producer
662    /// generation so stale writers are fenced.
663    ///
664    /// The client negotiates `InitProducerId` v5 or lower, as Kafka's
665    /// `FenceProducersHandler` does. v6 is an unstable version.
666    ///
667    /// The call retries as Apache Kafka's `FenceProducersHandler.handleError` does:
668    ///
669    /// - `COORDINATOR_LOAD_IN_PROGRESS` (14) and `CONCURRENT_TRANSACTIONS` (51): send the request again to the same coordinator.
670    /// - `COORDINATOR_NOT_AVAILABLE` (15) and `NOT_COORDINATOR` (16): find the
671    ///   coordinator again, then send the request again.
672    ///
673    /// A `FindCoordinator` answer of 14 or 15, and a failed or lost connection
674    /// to the coordinator (including a TLS or SASL handshake that stops
675    /// before the broker gives a verdict), also make the call find the
676    /// coordinator again. The call waits between attempts with Kafka's
677    /// backoff, and it stops when Kafka's default `default.api.timeout.ms`
678    /// (60 s) elapses. A call whose retries are unresolved at the deadline, or
679    /// whose attempt is still running, fails with a timeout error, as Kafka
680    /// gives a `TimeoutException`.
681    ///
682    /// # Errors
683    ///
684    /// Returns [`AdminError::Protocol`] for an empty transactional ID, or the
685    /// coordinator lookup, connection, transport, and broker errors returned
686    /// by Kafka. Returns [`ClientError::IncompatibleVersion`] when the
687    /// coordinator does not support `InitProducerId` v5 or lower.
688    ///
689    /// [`ClientError::IncompatibleVersion`]: krabka_client_core::ClientError::IncompatibleVersion
690    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    /// `force_terminate_transaction` with a call deadline that already runs,
714    /// so that every ID of one `fence_producers` call shares one deadline.
715    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    /// Fences the current producer generation and aborts any ongoing
737    /// transaction for each ID in `transactional_ids`, as Kafka's
738    /// `fenceProducers` operation does.
739    ///
740    /// Two transactional IDs can live on two different coordinators, so the
741    /// client fences them at the same time rather than one at a time. All IDs
742    /// share one call deadline, as Kafka's `FenceProducersHandler` and
743    /// `AdminApiDriver` run one `invokeDriver` for the whole call. The result
744    /// has one entry for each ID in `transactional_ids`, in no particular
745    /// order.
746    ///
747    /// See [`Self::force_terminate_transaction`] for the retry behavior and
748    /// the `InitProducerId` version that the client negotiates.
749    ///
750    /// # Errors
751    ///
752    /// Each entry gets [`AdminError::Protocol`] for an empty transactional
753    /// ID, or the coordinator lookup, connection, transport, and broker
754    /// errors returned by Kafka for that ID.
755    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    /// Fences the current producer generation and aborts the ongoing
780    /// transaction that wrote to `topic`-`partition`, without contacting the
781    /// transaction coordinator.
782    ///
783    /// This is Kafka's `abortTransaction` operation (KIP-664): it sends
784    /// `WriteTxnMarkers` straight to the partition leader with the identity of
785    /// the transaction to fence, as `AbortTransactionHandler` does. A
786    /// consumer that reads `describe_producers` or the partition's records
787    /// supplies `spec`.
788    ///
789    /// The call retries as Apache Kafka's
790    /// `AbortTransactionHandler.handleResponse` does: `NOT_LEADER_OR_FOLLOWER`
791    /// (6) and `LEADER_NOT_AVAILABLE` (5), from either the leader lookup or
792    /// the `WriteTxnMarkers` answer, look the leader up again. A failed or
793    /// lost connection to the leader does the same. Every other broker error
794    /// is final. The call waits between attempts with Kafka's backoff, and it
795    /// stops when Kafka's default `default.api.timeout.ms` (60 s) elapses.
796    ///
797    /// # Errors
798    ///
799    /// Returns [`AdminError::Protocol`] for an empty topic name, or when the
800    /// broker's answer names no result for the requested producer, topic, or
801    /// partition. Returns [`AdminError::Broker`] when the leader lookup or the
802    /// `WriteTxnMarkers` call fails, such as with
803    /// `TRANSACTIONAL_ID_AUTHORIZATION_FAILED` or `FENCED_INSTANCE_ID`.
804    /// Returns [`AdminError::Transport`] when the connection to the leader
805    /// fails.
806    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    /// One attempt of `abort_transaction`.
824    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    /// Returns the connection to the leader of `topic`-`partition`. When
846    /// `find_leader` is set, or when no connection is open, the attempt first
847    /// fetches fresh metadata and connects to the leader that it names.
848    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    /// Reads the producer state of each requested partition, as Kafka's
883    /// `describeProducers` operation does (KIP-664).
884    ///
885    /// The client finds the leader of each partition with `Metadata` and
886    /// sends one `DescribeProducers` request to each leader, all at the same
887    /// time, batching every partition of that leader into it, as Kafka's
888    /// `DescribeProducersHandler` with its `PartitionLeaderStrategy` does. The
889    /// result has one entry for each requested `(topic, partition)` pair.
890    ///
891    /// A partition goes back to the leader lookup, and the call finds its
892    /// leader again after the backoff, when:
893    ///
894    /// - the lookup names no leader yet, such as for an unknown topic or a
895    ///   partition with `LEADER_NOT_AVAILABLE`;
896    /// - the connection to its leader fails or is lost;
897    /// - the leader answers `NOT_LEADER_OR_FOLLOWER` (6) for it.
898    ///
899    /// A slow or failed leader does not delay the other leaders. The call
900    /// stops at Kafka's default `default.api.timeout.ms` (60 s), where each
901    /// unresolved partition gets `REQUEST_TIMED_OUT` (7).
902    ///
903    /// # Errors
904    ///
905    /// The call itself does not fail. Each partition gets its own
906    /// [`KafkaError`] in the result, such as the final lookup or
907    /// `DescribeProducers` error of that partition.
908    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    /// Lists every transaction that the cluster's coordinators currently
922    /// hold, as Kafka's `listTransactions` operation does (KIP-664).
923    ///
924    /// The client fetches the broker list from `Metadata` and sends
925    /// `ListTransactions` to every broker at the same time, since each
926    /// transaction coordinator answers only for the transactions it
927    /// coordinates. `filter` narrows the state, producer ID, and minimum
928    /// duration of the listed transactions, as Kafka's
929    /// `ListTransactionsOptions` does; the default matches every transaction.
930    ///
931    /// # Errors
932    ///
933    /// Returns [`AdminError::Broker`] when any broker gives a top-level
934    /// error, and [`AdminError::Transport`] when the connection to any broker
935    /// fails. A partial listing from the healthy brokers is not returned;
936    /// callers that need per-broker results should read
937    /// [`AdminError`] for which broker failed and retry.
938    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    /// One attempt of `force_terminate_transaction`.
962    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    /// Reads the transaction coordinator's current state for one transactional
984    /// ID.
985    ///
986    /// This is Kafka's `describeTransactions` operation (KIP-664). The client
987    /// discovers the transaction coordinator for `transactional_id` and sends
988    /// `DescribeTransactions` to that coordinator. An external system calls
989    /// this to verify the authority of a writer. It joins no group and holds
990    /// no producer state. The returned producer ID and producer epoch are the
991    /// only generation from which the coordinator accepts writes.
992    ///
993    /// The call retries as Apache Kafka's `DescribeTransactionsHandler.handleError` does:
994    ///
995    /// - `COORDINATOR_LOAD_IN_PROGRESS` (14): send the request again to the same coordinator.
996    /// - `COORDINATOR_NOT_AVAILABLE` (15) and `NOT_COORDINATOR` (16): find the
997    ///   coordinator again, then send the request again.
998    ///
999    /// A `FindCoordinator` answer of 14 or 15, and a failed or lost connection
1000    /// to the coordinator (including a TLS or SASL handshake that stops
1001    /// before the broker gives a verdict), also make the call find the
1002    /// coordinator again. The call waits between attempts with Kafka's
1003    /// backoff, and it stops when Kafka's default `default.api.timeout.ms`
1004    /// (60 s) elapses. A call whose retries are unresolved at the deadline, or
1005    /// whose attempt is still running, fails with a timeout error, as Kafka
1006    /// gives a `TimeoutException`.
1007    ///
1008    /// # Errors
1009    ///
1010    /// Returns [`AdminError::Protocol`] for an empty transactional ID, and for
1011    /// a response that carries no row for `transactional_id`. Returns
1012    /// [`AdminError::Broker`] when the coordinator lookup fails, or when the
1013    /// coordinator attaches an error code to the row, such as
1014    /// `TRANSACTIONAL_ID_NOT_FOUND`,
1015    /// `TRANSACTIONAL_ID_AUTHORIZATION_FAILED`, or `NOT_COORDINATOR`. Returns
1016    /// [`AdminError::Transport`] when the connection to the coordinator fails.
1017    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    /// `describe_transaction` with a call deadline that already runs, so that
1041    /// the IDs of one `describe_transactions` call share one deadline.
1042    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    /// One attempt of `describe_transaction`.
1064    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    /// Returns the transaction coordinator of `transactional_id`. When
1092    /// `find_coordinator` is set, or when no connection is open, the attempt
1093    /// first sends `FindCoordinator` and connects to the coordinator that it
1094    /// names.
1095    ///
1096    /// A `FindCoordinator` answer of `COORDINATOR_LOAD_IN_PROGRESS` (14) or
1097    /// `COORDINATOR_NOT_AVAILABLE` (15) and a failed connection to the
1098    /// coordinator give [`RetryAction::FindCoordinator`], as Kafka's
1099    /// `CoordinatorStrategy.handleError` and `AdminApiDriver.onFailure` do.
1100    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    /// Reads the transaction coordinator's current state for each ID in
1140    /// `transactional_ids`.
1141    ///
1142    /// Two transactional IDs can live on two different coordinators, so the
1143    /// client describes them one at a time. Each ID costs one
1144    /// `FindCoordinator` round trip, one new connection to the coordinator
1145    /// that the lookup names, and one `DescribeTransactions` round trip. The
1146    /// cost grows linearly with the number of IDs. Callers that describe a
1147    /// large set should expect that cost. The results keep the order of
1148    /// `transactional_ids`.
1149    ///
1150    /// # Errors
1151    ///
1152    /// Stops at the first ID that fails and returns its error. Returns
1153    /// [`AdminError::Protocol`] for an empty transactional ID, and for a
1154    /// response that carries no row for the ID. Returns
1155    /// [`AdminError::Broker`] when the coordinator lookup fails, or when the
1156    /// coordinator attaches an error code to the row. Returns
1157    /// [`AdminError::Transport`] when the connection to a coordinator fails.
1158    pub async fn describe_transactions(
1159        &self,
1160        transactional_ids: &[&str],
1161    ) -> Result<Vec<TransactionDescription>, AdminError> {
1162        // Kafka's `describeTransactions` runs all IDs under one deadline
1163        // (`invokeDriver` with one `deadlineMs`).
1164        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    /// The `InitProducerId` request that fences `payments` with no producer
1232    /// identity and no kept prepared transaction.
1233    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    /// The body of `response` at `version`, behind an empty tagged-field
1253    /// byte when `flexible` is set.
1254    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    /// An `ApiVersions` response that advertises `InitProducerId` in
1266    /// `init_producer_id_range`.
1267    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    /// The negotiated `InitProducerId` version and the result of
1294    /// `force_terminate_transaction`, with a version error as its ranges.
1295    type FenceResult = Result<(), (i16, i16, i16, i16, i16)>;
1296
1297    /// Apache Kafka's `FenceProducersHandler` builds `InitProducerId` with
1298    /// `new InitProducerIdRequest.Builder(data)`, which allows only released
1299    /// versions. v6 is unstable, so the request stops at v5.
1300    #[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    /// Kafka's `CoordinatorStrategy` reads the batched entry of the key from
1402    /// v4 on, and the top-level fields of an older answer.
1403    #[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    /// Kafka's `DescribeTransactionsHandler.handleResponse` names the
1457    /// coordinator that answered, maps a negative start time to an empty
1458    /// `OptionalLong`, and collects the partitions of every topic.
1459    #[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    /// The transaction admin call of a retry case.
1586    #[derive(Clone, Copy, Debug)]
1587    enum TransactionCall {
1588        Describe,
1589        ForceTerminate,
1590    }
1591
1592    /// The error codes that the mock brokers answer, one per request. The last
1593    /// code repeats.
1594    #[derive(Default)]
1595    struct CoordinatorScript {
1596        find_coordinator: Vec<i16>,
1597        coordinator: Vec<i16>,
1598        find_coordinator_requests: usize,
1599        coordinator_requests: usize,
1600        /// The first `unreachable_answers` `FindCoordinator` answers name
1601        /// `unreachable`, an address that refuses connections.
1602        unreachable_answers: usize,
1603        unreachable: Option<SocketAddr>,
1604        /// The coordinator does not answer the transaction request.
1605        silent: bool,
1606        /// The first `closed_find_coordinators` `FindCoordinator` requests
1607        /// close the connection with no answer.
1608        closed_find_coordinators: usize,
1609    }
1610
1611    /// How the coordinator of a retry case behaves.
1612    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1613    enum CoordinatorBehavior {
1614        /// It answers from the script.
1615        Normal,
1616        /// The first `FindCoordinator` answer names an address that refuses
1617        /// connections.
1618        UnreachableOnce,
1619        /// It does not answer the transaction request.
1620        Silent,
1621        /// The bootstrap broker closes the connection on the first
1622        /// `FindCoordinator`.
1623        FindCoordinatorClosedOnce,
1624    }
1625
1626    /// An address on which no listener accepts connections.
1627    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    /// An `ApiVersions` response for the retry mocks.
1639    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    /// A mock broker that answers `FindCoordinator` with `coordinator`, and
1663    /// `DescribeTransactions` and `InitProducerId` with the codes of `script`.
1664    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    /// A call that stops at its deadline gives a timeout. The retry tests give
1755    /// it Kafka's `REQUEST_TIMED_OUT` code.
1756    const TIMED_OUT: i16 = 7;
1757    /// A deadline that several attempts fit in.
1758    const LONG: Duration = Duration::from_secs(5);
1759    /// One attempt fits in `NOW`, and the backoff after it reaches the
1760    /// deadline, so the call makes no second attempt.
1761    const NOW: Duration = Duration::from_millis(500);
1762    /// A deadline shorter than the request timeout.
1763    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    /// Run one retry case against a scripted bootstrap broker and
1783    /// coordinator.
1784    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        // A call never runs far past its deadline.
1848        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    /// Apache Kafka's `DescribeTransactionsHandler.handleError` retries
1861    /// `COORDINATOR_LOAD_IN_PROGRESS` (14) on the same coordinator.
1862    /// `FenceProducersHandler.handleError` retries 14 and
1863    /// `CONCURRENT_TRANSACTIONS` (51) on the same coordinator. Both unmap the
1864    /// transactional ID on `COORDINATOR_NOT_AVAILABLE` (15) and
1865    /// `NOT_COORDINATOR` (16), so the driver finds the coordinator again, and
1866    /// fail on every other code. `CoordinatorStrategy.handleError` retries a
1867    /// `FindCoordinator` answer of 14 or 15. The driver stops at the call
1868    /// timeout.
1869    #[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    /// Kafka's `AdminApiDriver.onFailure` looks the coordinator up again
2008    /// after a disconnect, and `KafkaAdminClient` times out a call in flight
2009    /// at its deadline.
2010    #[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    /// Kafka's `DescribeTransactionsHandler` names the broker that answered
2069    /// as the coordinator, which is the node that `FindCoordinator` gave.
2070    #[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    /// Kafka's `describeTransactions` runs every ID under one call deadline
2108    /// (`invokeDriver`), so a slow first ID leaves less time for the next.
2109    #[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        // The first ID needs about 800 ms. The second ID gets the rest of the
2144        // 1.5 s deadline, not a new 1.5 s.
2145        assert2::assert!(matches!(
2146            result,
2147            Err(AdminError::Transport(ClientError::Timeout(_)))
2148        ));
2149        assert2::assert!(elapsed < Duration::from_millis(2200), "{elapsed:?}");
2150    }
2151
2152    /// The transaction that the abort tests fence.
2153    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    /// A `Metadata` answer in which broker 1 at `addr` leads `orders`-0 and
2164    /// `orders`-1, with `partition_error` on each partition, and `missing`
2165    /// is an unknown topic.
2166    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    /// The outcome of an admin call reduced to what the tests compare: the
2197    /// failing API and error code, or `("protocol", 0)` for a protocol error.
2198    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    /// A `WriteTxnMarkers` answer with `code` for `producer_id` on
2287    /// `topic`-`partition`.
2288    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    /// Apache Kafka's `AbortTransactionHandler.handleResponse` looks the
2343    /// leader up again after `NOT_LEADER_OR_FOLLOWER` and
2344    /// `LEADER_NOT_AVAILABLE`, and fails on every other code.
2345    #[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    /// The `DescribeProducers` answer of the tests: one producer with an open
2399    /// transaction and one without on `orders`-0, `NOT_LEADER_OR_FOLLOWER` on
2400    /// `orders`-1, and a partition that nobody asked about.
2401    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    /// The per-partition results of [`orders_producers`] for `orders`-0 and
2443    /// `orders`-1.
2444    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    /// Decodes the request body of a mock broker: the client id, the tagged
2528    /// field byte of a flexible header, and the request itself.
2529    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    /// An `ApiVersions` response for the leader-routed mocks.
2539    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    /// What a leader-routed mock broker saw and answers.
2562    #[derive(Default)]
2563    struct LeaderScript {
2564        /// The `WriteTxnMarkers` error codes, one per request. The last
2565        /// repeats.
2566        write_codes: Vec<i16>,
2567        /// The `WriteTxnMarkers` requests that the broker received.
2568        write_requests: Vec<WriteTxnMarkersRequest>,
2569        /// The `Metadata` requests that the broker received.
2570        metadata_requests: usize,
2571        /// The `ListTransactions` requests that the broker received.
2572        list_requests: Vec<ListTransactionsRequest>,
2573        /// The `ListTransactions` answer of the broker.
2574        listing: ListTransactionsResponse,
2575    }
2576
2577    /// A mock broker that leads every `orders` partition. `cluster` names the
2578    /// brokers that its `Metadata` answer lists, itself first.
2579    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    /// An admin client on `bootstrap` that retries after 1 ms.
2630    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    /// Apache Kafka's `AbortTransactionHandler` sends one `WriteTxnMarkers`
2646    /// to the partition leader, looks the leader up again after
2647    /// `NOT_LEADER_OR_FOLLOWER`, and fails on every other code.
2648    #[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    /// The `DescribeProducers` answer to `request`: the producers of
2709    /// [`orders_producers`] on `orders`-0, and `orders_1_code` on `orders`-1.
2710    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    /// Apache Kafka's `DescribeProducersHandler` batches the partitions of
2746    /// one leader into one request, gives each partition its own result, and
2747    /// sends a partition back to the `PartitionLeaderStrategy` lookup after
2748    /// `NOT_LEADER_OR_FOLLOWER` or a lost connection to its leader.
2749    #[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    /// A `ListTransactions` answer with `error_code` and one transaction per
2839    /// ID in `transactional_ids`.
2840    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    /// Apache Kafka's `ListTransactionsHandler` asks every broker
2858    /// (`AllBrokersStrategy`) and merges the listings. A broker error fails
2859    /// the call.
2860    #[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    /// Apache Kafka's `fenceProducers` gives each transactional ID its own
2928    /// result, and an invalid ID does not stop the others.
2929    #[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    /// A caller can spawn every retrying admin call on a multi-thread
2977    /// runtime, so each future must be `Send`.
2978    #[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}