Skip to main content

krabka_client_producer/
producer.rs

1//! The public `Producer` type. The builder lives in `builder.rs`, and the
2//! sender task lives in `sender.rs`.
3
4use std::{
5    collections::HashMap,
6    fmt,
7    sync::{
8        Arc, Mutex as SyncMutex, MutexGuard as SyncMutexGuard, PoisonError,
9        atomic::{AtomicBool, AtomicI16, AtomicI64, AtomicU8, AtomicU64, AtomicUsize, Ordering},
10    },
11    time::{Duration, SystemTime, UNIX_EPOCH},
12};
13
14use bytes::BufMut;
15use dashmap::DashMap;
16use krabka_client_consumer::ConsumerGroupMetadata;
17use krabka_client_core::{
18    Client, ClientError, ClientFrameMax, ConnectionDispatchQueueCapacity, CoordinatorKeyType,
19    build_find_coordinator, security::ClientSecurity,
20};
21use krabka_protocol::{
22    Encode, ProtocolError, ProtocolRequest,
23    owned::{
24        add_offsets_to_txn_request::AddOffsetsToTxnRequest,
25        add_partitions_to_txn_request::{self, AddPartitionsToTxnRequest},
26        add_partitions_to_txn_response::AddPartitionsToTxnResponse,
27        common::add_partitions_to_txn_request::add_partitions_to_txn_topic::AddPartitionsToTxnTopic,
28        end_txn_request::EndTxnRequest,
29        find_coordinator_response::FindCoordinatorResponse,
30        init_producer_id_request::InitProducerIdRequest,
31        init_producer_id_response::InitProducerIdResponse,
32        metadata_response::MetadataResponse,
33        txn_offset_commit_request::{
34            TxnOffsetCommitRequest, TxnOffsetCommitRequestPartition, TxnOffsetCommitRequestTopic,
35        },
36        txn_offset_commit_response::TxnOffsetCommitResponse,
37    },
38    primitives::uuid::Uuid,
39};
40use krabka_units::{ByteSize, Time, convert::TimeExt};
41use tokio::{
42    sync::{Mutex, MutexGuard, Notify, mpsc},
43    task::JoinHandle,
44    time::{self, Instant},
45};
46use tokio_util::sync::CancellationToken;
47use tracing::{Instrument as _, field};
48
49use crate::{
50    accumulator::{
51        Accumulator, AccumulatorMap, AppendResult, approx_record_size, record_size_upper_bound,
52    },
53    buffer_pool::BufferPool,
54    builder::{ProducerFlushTimeout, send_init_producer_id},
55    compression::{Compression, CompressionLevels},
56    error::{ProducerError, RecordSizeLimit},
57    metadata_wait::{MetadataRefresh, MetadataWait, metadata_request},
58    partitioner::{
59        BuiltInPartitioner, PartitionInfo, Partitioner, RackPreference, StickyPartition,
60        TopicPartitions,
61    },
62    record::{DeliveryHandle, ProducerRecord, RecordMetadata},
63    sender::DrainIntent,
64    transactional::{
65        AbortableError, FatalError, OwnedTransaction, PreparedTransactionState, Transaction,
66        TxnErrorSlot, TxnState,
67    },
68    txn_retry::{
69        self, AddPartitionsDecision, CoordinatorAttempt, EndTxnDecision, FindCoordinatorDecision,
70        LookupRefusal, TxnRequestDecision,
71    },
72};
73
74/// The last `AddPartitionsToTxn` version that a client sends.
75const ADD_PARTITIONS_LAST_CLIENT_VERSION: i16 = 3;
76
77/// An `AddPartitionsToTxn` request from a client, capped at v3.
78///
79/// v4 and later carry the broker-to-broker form of the request, which a Kafka
80/// broker answers only for a principal with `CLUSTER_ACTION` on the cluster.
81/// Apache Kafka's client builds every such request with
82/// `AddPartitionsToTxnRequest.Builder.forClient`, which allows
83/// `ApiKeys.ADD_PARTITIONS_TO_TXN.oldestVersion()` to `LAST_CLIENT_VERSION`
84/// (3), and fills only the `v3_and_below` fields. This type gives the same cap
85/// to version negotiation.
86#[derive(Clone, Debug, PartialEq, Eq)]
87struct ClientAddPartitionsToTxn(AddPartitionsToTxnRequest);
88
89impl Encode for ClientAddPartitionsToTxn {
90    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
91        self.0.encode(buf, version)
92    }
93
94    fn encoded_len(&self, version: i16) -> usize {
95        self.0.encoded_len(version)
96    }
97}
98
99impl ProtocolRequest for ClientAddPartitionsToTxn {
100    const API_KEY: i16 = add_partitions_to_txn_request::API_KEY;
101    const MIN_VERSION: i16 = add_partitions_to_txn_request::MIN_VERSION;
102    const MAX_VERSION: i16 = ADD_PARTITIONS_LAST_CLIENT_VERSION;
103    /// The cap is a released version, so it is also the stable maximum.
104    const LATEST_STABLE_VERSION: i16 = Self::MAX_VERSION;
105    const FLEXIBLE_MIN: i16 = add_partitions_to_txn_request::FLEXIBLE_MIN;
106    type Response = AddPartitionsToTxnResponse;
107}
108
109/// The last `EndTxn` version before transaction version 2. Kafka's
110/// `EndTxnRequest.LAST_STABLE_VERSION_BEFORE_TRANSACTION_V2`.
111const END_TXN_TRANSACTION_V1_MAX_VERSION: i16 = 4;
112
113/// The last `TxnOffsetCommit` version before transaction version 2. Kafka's
114/// `TxnOffsetCommitRequest.LAST_STABLE_VERSION_BEFORE_TRANSACTION_V2`.
115const TXN_OFFSET_COMMIT_TRANSACTION_V1_MAX_VERSION: i16 = 4;
116
117/// The last `TxnOffsetCommit` version that names each topic. v6 (KIP-1319)
118/// names each topic by id only. Kafka's `TxnOffsetCommitRequest.Builder`
119/// `.forTopicNames` stops here with transaction version 2.
120const TXN_OFFSET_COMMIT_TOPIC_NAME_MAX_VERSION: i16 = 5;
121
122/// The finalized feature that names the transaction protocol of the cluster.
123const TRANSACTION_VERSION_FEATURE: &str = "transaction.version";
124
125/// A request that negotiates `MAX` at most.
126///
127/// The version of `EndTxn`, `TxnOffsetCommit` and `Produce` tells a broker
128/// which transaction protocol the producer follows. A producer on transaction
129/// version 1 caps each of them at the last version before transaction version
130/// 2, as Kafka's request builders do when `isTransactionV2Enabled` is false.
131#[derive(Clone, Debug, PartialEq, Eq)]
132struct VersionCapped<R, const MAX: i16>(R);
133
134impl<R: Encode, const MAX: i16> Encode for VersionCapped<R, MAX> {
135    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
136        self.0.encode(buf, version)
137    }
138
139    fn encoded_len(&self, version: i16) -> usize {
140        self.0.encoded_len(version)
141    }
142}
143
144impl<R: ProtocolRequest, const MAX: i16> ProtocolRequest for VersionCapped<R, MAX> {
145    const API_KEY: i16 = R::API_KEY;
146    const MIN_VERSION: i16 = R::MIN_VERSION;
147    const MAX_VERSION: i16 = MAX;
148    /// The cap is what the client offers, so the negotiation ends here too.
149    const LATEST_STABLE_VERSION: i16 = MAX;
150    const FLEXIBLE_MIN: i16 = R::FLEXIBLE_MIN;
151    type Response = R::Response;
152}
153
154/// Send `request` on `client` in the transaction protocol that
155/// `transaction_v2` names: at the latest version for transaction version 2,
156/// and at `V1_MAX` at most for transaction version 1.
157async fn send_in_transaction_version<R, const V1_MAX: i16>(
158    client: &Client,
159    request: R,
160    transaction_v2: bool,
161) -> Result<R::Response, ClientError>
162where
163    R: ProtocolRequest + Send + Sync,
164{
165    if transaction_v2 {
166        client.send(request).await
167    } else {
168        client.send(VersionCapped::<R, V1_MAX>(request)).await
169    }
170}
171
172/// The `TxnOffsetCommit` version cap of one request.
173///
174/// Kafka's `TransactionManager.txnOffsetCommitHandler` picks
175/// `TxnOffsetCommitRequest.Builder.forTopicIdsOrNames` when the metadata
176/// cache holds an id for every topic, and `forTopicNames` otherwise.
177/// `forTopicIdsOrNames` allows the latest version, and `forTopicNames` stops
178/// at v5. With transaction version 1 both stop at
179/// `LAST_STABLE_VERSION_BEFORE_TRANSACTION_V2`.
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181enum TxnOffsetCommitCap {
182    /// The producer follows transaction version 1, so the request stops at
183    /// v4.
184    TransactionV1,
185    /// A topic of the request has no topic id, so the request stops at v5,
186    /// which names each topic.
187    TopicNames,
188    /// Every topic has an id, so the request can use v6, which names each
189    /// topic by id.
190    Latest,
191}
192
193impl TxnOffsetCommitCap {
194    /// The cap of a request whose topics are `topics`.
195    fn of(topics: &[TxnOffsetCommitRequestTopic], transaction_v2: bool) -> Self {
196        if !transaction_v2 {
197            Self::TransactionV1
198        } else if topics.iter().any(|topic| topic.topic_id == Uuid::ZERO) {
199            Self::TopicNames
200        } else {
201            Self::Latest
202        }
203    }
204
205    /// The highest version that the cap offers.
206    #[cfg(test)]
207    const fn max_version(self) -> i16 {
208        match self {
209            Self::TransactionV1 => TXN_OFFSET_COMMIT_TRANSACTION_V1_MAX_VERSION,
210            Self::TopicNames => TXN_OFFSET_COMMIT_TOPIC_NAME_MAX_VERSION,
211            Self::Latest => <TxnOffsetCommitRequest as ProtocolRequest>::LATEST_STABLE_VERSION,
212        }
213    }
214
215    /// Send `request` on `client`, and negotiate at most the version of the
216    /// cap.
217    async fn send(
218        self,
219        client: &Client,
220        request: TxnOffsetCommitRequest,
221    ) -> Result<TxnOffsetCommitResponse, ClientError> {
222        match self {
223            Self::TransactionV1 => {
224                client
225                    .send(VersionCapped::<
226                        _,
227                        TXN_OFFSET_COMMIT_TRANSACTION_V1_MAX_VERSION,
228                    >(request))
229                    .await
230            }
231            Self::TopicNames => {
232                client
233                    .send(VersionCapped::<_, TXN_OFFSET_COMMIT_TOPIC_NAME_MAX_VERSION>(request))
234                    .await
235            }
236            Self::Latest => client.send(request).await,
237        }
238    }
239}
240
241/// Tell if the finalized features of a broker turn on transaction version 2.
242/// Kafka's `TransactionManager.maybeUpdateTransactionV2Enabled`:
243/// `transactionVersion != null && transactionVersion >= 2`.
244fn transaction_v2_enabled(features: &krabka_client_core::FinalizedFeatures) -> bool {
245    features
246        .level(TRANSACTION_VERSION_FEATURE)
247        .is_some_and(|level| level >= 2)
248}
249
250/// The deadline and the backoff of the retries of one coordinator request.
251struct CoordinatorRetry {
252    deadline: Instant,
253    backoff: Duration,
254    max_backoff: Duration,
255}
256
257impl CoordinatorRetry {
258    /// Wait before the next attempt, and give `false` when the deadline has
259    /// passed. The wait doubles up to the maximum backoff. A wait that reaches
260    /// the deadline gives `false`, so no attempt starts after it.
261    async fn wait(&mut self) -> bool {
262        let remaining = self.deadline.saturating_duration_since(Instant::now());
263        if remaining.is_zero() {
264            return false;
265        }
266        time::sleep(self.backoff.min(remaining)).await;
267        self.backoff = self.backoff.saturating_mul(2).min(self.max_backoff);
268        Instant::now() < self.deadline
269    }
270}
271
272/// Why a `FindCoordinator` lookup gave no coordinator.
273#[derive(Debug, thiserror::Error)]
274enum LookupError {
275    /// The broker refused the lookup.
276    #[error("the broker refused the coordinator lookup: {0:?}")]
277    Refused(LookupRefusal),
278    /// The retry deadline ended, or the request cannot be sent. It carries
279    /// the last error: the retriable code, or the transport failure.
280    #[error("the coordinator lookup did not resolve: {0}")]
281    Unresolved(ProducerError),
282}
283
284impl From<ProducerError> for LookupError {
285    fn from(error: ProducerError) -> Self {
286        Self::Unresolved(error)
287    }
288}
289
290/// The error code and the `host:port` address of a `FindCoordinator` answer.
291///
292/// Kafka's `FindCoordinatorHandler` reads the first row of `coordinators`,
293/// which versions 4 and later return. `FindCoordinatorResponse.coordinators`
294/// builds the same row from the top-level fields of versions 0 to 3.
295fn coordinator_answer(response: &FindCoordinatorResponse) -> (i16, String) {
296    response.coordinators.first().map_or_else(
297        || {
298            (
299                response.error_code,
300                format!("{}:{}", response.host, response.port),
301            )
302        },
303        |row| (row.error_code, format!("{}:{}", row.host, row.port)),
304    )
305}
306
307/// Whether a `FindCoordinator` lookup that failed in transport can succeed
308/// later. Kafka's `NetworkClient` reports a failed connection and a closed
309/// connection as a disconnect, and `TxnRequestHandler.onComplete` sends the
310/// lookup again. An authentication failure, a version mismatch and a codec
311/// error repeat on every attempt.
312fn lookup_may_succeed_later(error: &ClientError) -> bool {
313    matches!(
314        error,
315        ClientError::Disconnected
316            | ClientError::Timeout(_)
317            | ClientError::Io(_)
318            | ClientError::Connect { .. }
319            | ClientError::Tls { .. }
320            | ClientError::Sasl { .. }
321    )
322}
323
324/// Whether a request can have reached the broker, so the producer can send it
325/// again. A failure before the send, such as a version error or a codec error,
326/// repeats on every attempt, so the caller reports it at once.
327fn request_may_have_reached_the_broker(error: &ClientError) -> bool {
328    matches!(
329        error,
330        ClientError::Disconnected | ClientError::Timeout(_) | ClientError::Io(_)
331    )
332}
333
334/// The partition error code that decides a `TxnOffsetCommit` answer.
335///
336/// A code that no rule retries decides the answer, wherever its row is.
337/// Kafka's `TxnOffsetCommitHandler` leaves its loop at such a row, and it
338/// sends the request again only when every failed row is retriable. Among
339/// retriable rows, a row that asks for the coordinator again wins, because one
340/// such row makes Kafka's handler look the coordinator up for the whole
341/// request.
342fn txn_offset_commit_error_code(response: &TxnOffsetCommitResponse) -> i16 {
343    let codes = response
344        .topics
345        .iter()
346        .flat_map(|topic| {
347            topic
348                .partitions
349                .iter()
350                .map(|partition| partition.error_code)
351        })
352        .filter(|code| *code != 0);
353    let mut rediscover = None;
354    let mut resend = None;
355    for code in codes {
356        match txn_retry::decide_txn_offset_commit(CoordinatorAttempt::Answered(code)) {
357            TxnRequestDecision::Retry { rediscover: true } => {
358                rediscover.get_or_insert(code);
359            }
360            TxnRequestDecision::Retry { rediscover: false } => {
361                resend.get_or_insert(code);
362            }
363            _ => return code,
364        }
365    }
366    rediscover.or(resend).unwrap_or(0)
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum Acks {
371    Zero,
372    One,
373    All,
374}
375
376impl Acks {
377    #[must_use]
378    pub fn wire(self) -> i16 {
379        match self {
380            Self::Zero => 0,
381            Self::One => 1,
382            Self::All => -1,
383        }
384    }
385}
386
387/// Tri-state lifecycle.
388pub const STATE_ACTIVE: u8 = 0;
389pub const STATE_FENCED: u8 = 1;
390pub const STATE_CLOSED: u8 = 2;
391
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct TopicMetadata {
394    pub num_partitions: i32,
395    /// Topic UUID. Produce v13+ needs it, because that version encodes only
396    /// the `topic_id` on the wire. Zero, `Uuid::ZERO`, is a valid sentinel that
397    /// means "not yet known". For older wire versions the broker falls back to
398    /// the `name` field.
399    pub topic_id: Uuid,
400}
401
402/// The partition count and id of each topic that the producer tracks, shared
403/// by the application's sends, the sender task and the metadata refresh.
404///
405/// The lock is a synchronous mutex, as Kafka's `ProducerMetadata` guards its
406/// state with `synchronized`. The sender task keeps every in-flight Produce as
407/// a sub-future that it polls only from its `select!`. With an async mutex, a
408/// send that was polled once while the application held the lock waited in the
409/// mutex queue, and the unlock handed the lock to it. The sender body then
410/// waited for the same lock in its next drain, and never returned to the
411/// `select!` that would poll the send: the producer stopped. A synchronous
412/// guard cannot be held across an `.await` (Clippy's `await_holding_lock`), so
413/// every holder runs to its unlock without yielding, and no unpolled future can
414/// hold or queue for the lock.
415#[derive(Debug, Default)]
416pub struct MetadataCache(SyncMutex<HashMap<String, TopicMetadata>>);
417
418impl MetadataCache {
419    /// Lock the cache. Never hold the guard across an `.await`.
420    pub(crate) fn lock(&self) -> SyncMutexGuard<'_, HashMap<String, TopicMetadata>> {
421        // A poisoned lock means that a thread panicked while it held the
422        // guard. Every holder makes single map lookups, inserts, removes and
423        // field stores, so the map stays whole after such a panic, and each
424        // entry is metadata that the next refresh corrects anyway. The cache
425        // stays usable rather than failing every later send.
426        self.0.lock().unwrap_or_else(PoisonError::into_inner)
427    }
428}
429
430impl From<HashMap<String, TopicMetadata>> for MetadataCache {
431    fn from(topics: HashMap<String, TopicMetadata>) -> Self {
432        Self(SyncMutex::new(topics))
433    }
434}
435
436#[derive(Clone, Debug)]
437pub struct ProducerIdentity {
438    /// Shared with the sender so ID rotation remains observable.
439    pub id: Arc<AtomicI64>,
440    /// The idempotent producer epoch. The sender task shares it, and it raises
441    /// the epoch when Kafka's idempotent producer does.
442    pub epoch: Arc<AtomicI16>,
443}
444
445fn wake_sender_after_append(wake_tx: &mpsc::Sender<DrainIntent>, linger: Time, wakes_sender: bool) {
446    if linger == <Time as TimeExt>::ZERO {
447        let _ = wake_tx.try_send(DrainIntent::Force);
448    } else if wakes_sender {
449        let _ = wake_tx.try_send(DrainIntent::Ready);
450    }
451}
452
453/// Kafka's `send.buffer.bytes` and `receive.buffer.bytes` of a producer.
454/// `None` keeps the operating system default.
455#[derive(Clone, Copy, Debug, PartialEq)]
456pub struct SocketBuffers {
457    pub(crate) send: Option<ByteSize>,
458    pub(crate) receive: Option<ByteSize>,
459}
460
461// accumulators map is inherently complex
462pub struct Producer {
463    pub(crate) client: Client,
464    pub(crate) client_id: String,
465    /// TLS and SASL security policy used for the bootstrap connection.
466    ///
467    /// The producer retains it so that every secondary connection it opens
468    /// after construction carries the same credentials. Those are the
469    /// transaction-coordinator and group-coordinator dials in the transactional
470    /// path. Without it, those connections would be plaintext and
471    /// unauthenticated, a secured listener would drop them, and the
472    /// transactional flow would fail with `Client(Disconnected)`.
473    pub(crate) security: Option<ClientSecurity>,
474    pub(crate) dns_timeout: Time,
475    pub(crate) metadata_recovery_strategy: krabka_client_core::MetadataRecoveryStrategy,
476    pub(crate) metadata_recovery_rebootstrap_trigger:
477        krabka_client_core::MetadataRecoveryRebootstrapTrigger,
478    pub(crate) client_dns_lookup: krabka_client_core::ClientDnsLookup,
479    pub(crate) reconnect_backoff: Time,
480    pub(crate) reconnect_backoff_max: Time,
481    pub(crate) connections_max_idle: Time,
482    pub(crate) socket_connection_setup_timeout: Time,
483    pub(crate) socket_connection_setup_timeout_max: Time,
484    pub(crate) dispatch_queue_capacity: ConnectionDispatchQueueCapacity,
485    pub(crate) frame_max: ClientFrameMax,
486    /// The socket buffers of every connection, so that the coordinator
487    /// connections get those of the bootstrap connection.
488    pub(crate) socket_buffers: SocketBuffers,
489    pub(crate) identity: ProducerIdentity,
490    // The following config knobs are also copied into `SenderConfig` at
491    // construction time. They live on `Producer` for diagnostic
492    // introspection and to support future reconnect / re-init flows.
493    // Suppressing the dead-code warning is honest about
494    // their current role.
495    #[allow(dead_code)]
496    pub(crate) acks: Acks,
497    pub(crate) compression: Compression,
498    /// The validated `compression.<codec>.level` settings (KIP-390).
499    pub(crate) compression_levels: CompressionLevels,
500    pub(crate) batch_size: usize,
501    #[allow(dead_code)]
502    pub(crate) linger: Time,
503    #[allow(dead_code)]
504    pub(crate) request_timeout: Time,
505    pub(crate) flush_timeout: ProducerFlushTimeout,
506    /// The longest time that `send` waits for the metadata of its topic.
507    /// Kafka's `max.block.ms` gives the same limit to `waitOnMetadata`.
508    pub(crate) max_block: Duration,
509    /// The buffer memory of the batches. `send` waits for it, as Kafka's
510    /// `RecordAccumulator.append` waits for its `BufferPool`.
511    pub(crate) buffer_pool: BufferPool,
512    /// The largest serialized record that `send` accepts, in bytes. Kafka's
513    /// `max.request.size`.
514    pub(crate) max_request_size: usize,
515    #[allow(dead_code)]
516    pub(crate) max_in_flight: usize,
517    pub(crate) metadata_cache: Arc<MetadataCache>,
518    /// The metadata refreshes that concurrent sends share while they wait for
519    /// a topic. See `MetadataWait`.
520    pub(crate) metadata_refresh: Arc<MetadataRefresh>,
521    /// Per-`(topic, partition)` leader-id cache. The sender uses it to route
522    /// each Produce to the broker that actually leads the partition.
523    ///
524    /// `Metadata` fills it alongside the partition count; see `partition_count`.
525    /// A missing entry means the leader is unknown, and the sender falls back
526    /// to the bootstrap connection. A leader id `< 0` also counts as unknown.
527    /// An `Arc` shares the cache with the sender task.
528    pub(crate) partition_leaders: Arc<DashMap<(String, i32), i32>>,
529    /// Rack of each broker (node) id, keyed by `leader_id`. `partition_count`
530    /// fills it from each `Metadata` response's `brokers` list.
531    /// `partitioner.rack.aware` reads it through [`Producer::leader_rack`].
532    pub(crate) broker_racks: Arc<DashMap<i32, Option<String>>>,
533    pub(crate) accumulators: AccumulatorMap,
534    pub(crate) next_seq: Arc<DashMap<(String, i32), i32>>,
535    pub(crate) partitioner: Arc<BuiltInPartitioner>,
536    /// A caller-supplied `partitioner.class` hook. When set, `send` calls it
537    /// for a record with no partition instead of the built-in partitioner.
538    /// See [`Partitioner`].
539    pub(crate) custom_partitioner: Option<Arc<dyn Partitioner>>,
540    /// Kafka's `client.rack`. With `partitioner.rack.aware`
541    /// (`self.partitioner.config().rack_aware`), the built-in partitioner's
542    /// sticky pick prefers a partition whose leader is in this rack.
543    pub(crate) client_rack: Option<String>,
544    pub(crate) state: Arc<AtomicU8>,
545    pub(crate) wake_tx: mpsc::Sender<DrainIntent>,
546    pub(crate) flush_notify: Arc<Notify>,
547    /// Count of batches the sender has popped from an accumulator but has not
548    /// yet finished sending, that is, the Produce is in flight and awaits the
549    /// broker ack.
550    ///
551    /// A batch that has left the accumulator but is still in flight is
552    /// invisible to `all_empty`, so `flush` must also wait for this count to
553    /// reach zero. Otherwise `commit_transaction` can race ahead of the Produce
554    /// that drives the txn to `Ongoing`, and the coordinator rejects `EndTxn`
555    /// with `INVALID_TXN_STATE`.
556    pub(crate) in_flight: Arc<AtomicUsize>,
557    pub(crate) sender_shutdown: CancellationToken,
558    pub(crate) sender_handle: Option<JoinHandle<()>>,
559    pub(crate) transactional_id: Option<String>,
560    pub(crate) transaction_timeout_ms: i32,
561    pub(crate) two_phase_commit_enabled: bool,
562    pub(crate) init_retry_timeout: Time,
563    pub(crate) init_retry_backoff: Time,
564    pub(crate) retry_backoff_max: Time,
565    /// An `Arc` wraps it, so the sender task can share the same state without
566    /// more synchronization structures.
567    pub(crate) txn_state: Arc<Mutex<TxnState>>,
568    /// Set synchronously when an unresolved transaction guard is dropped, or
569    /// when `EndTxn` loses its response. This is separate from `txn_state`,
570    /// because `Drop` cannot await its async mutex.
571    pub(crate) txn_recovery_required: Arc<AtomicBool>,
572    pub(crate) txn_recovery_generation: Arc<AtomicU64>,
573    /// Odd values identify a live transaction guard; resolving or abandoning
574    /// that guard advances the counter to the following even value. Keeping
575    /// the generation on each guard prevents an old prepared guard from
576    /// poisoning a later transaction when it is eventually dropped.
577    pub(crate) txn_guard_generation: Arc<AtomicU64>,
578    /// Cached connection to the transaction coordinator broker.
579    /// `init_transactions` fills it, and begin, commit and abort reuse it.
580    pub(crate) txn_coord_client: Mutex<Option<Client>>,
581    /// Authoritative `(producer_id, producer_epoch)` for the transactional
582    /// flow. `init_transactions` sets it, and the sender reads it when it
583    /// builds transactional `ProduceRequest`s.
584    pub(crate) txn_pid_epoch: Arc<Mutex<(i64, i16)>>,
585    /// The error state of the transaction.
586    ///
587    /// The sender sets the abortable error when a transactional batch fails,
588    /// and a coordinator answer of an abortable code sets it too. `send`,
589    /// `prepare_transaction`, `commit` and `send_offsets_to_transaction` then
590    /// fail until the application aborts the transaction. A fence or a fatal
591    /// coordinator code sets the fatal error, and every later transactional
592    /// operation fails. Kafka's `TransactionManager` keeps the same states in
593    /// `ABORTABLE_ERROR` and `FATAL_ERROR` with `lastError`.
594    pub(crate) txn_error: Arc<TxnErrorSlot>,
595    /// The producer follows transaction version 2 (KIP-890 part 2). It skips
596    /// `AddPartitionsToTxn` and `AddOffsetsToTxn`, and sends `Produce`,
597    /// `EndTxn` and `TxnOffsetCommit` at their latest versions.
598    /// `init_transactions` sets it from the finalized feature
599    /// `transaction.version` of the transaction coordinator. The transport of
600    /// the sender shares it.
601    pub(crate) transaction_v2: Arc<AtomicBool>,
602    /// Identity of the transaction that was prepared locally or recovered via
603    /// `InitProducerId(keepPreparedTxn=true)`. Recovery deliberately keeps this
604    /// separate from `txn_pid_epoch`, which is the newly staged identity used
605    /// to send `EndTxn`.
606    pub(crate) prepared_transaction_state: Arc<Mutex<Option<PreparedTransactionState>>>,
607}
608
609/// Reads a minted transactional identity out of the stored pair.
610///
611/// The coordinator has minted a pair only when both halves are non-negative.
612/// `InitProducerId` stores `(-1, -1)` before the first call, and Kafka writes
613/// `-1` in either half for "no producer", so both halves are checked. A check
614/// of one half alone would report a half-written pair as a real identity, and
615/// a lease written under it would carry an epoch no broker can fence on.
616fn minted_identity(pair: (i64, i16)) -> Option<(i64, i16)> {
617    let (id, epoch) = pair;
618    (id >= 0 && epoch >= 0).then_some(pair)
619}
620
621/// Read the error code of the one partition in an `AddPartitionsToTxn`
622/// response.
623///
624/// Versions 0 to 3 carry the partition results in
625/// `results_by_topic_v3_and_below`, and versions 4 and later carry them in
626/// `results_by_transaction`. Kafka's `AddPartitionsToTxnResponse.errors` reads
627/// both. A response without a partition row gives the top-level code, which
628/// versions 4 and later carry.
629fn add_partitions_error_code(response: &AddPartitionsToTxnResponse) -> i16 {
630    response
631        .results_by_topic_v3_and_below
632        .first()
633        .or_else(|| {
634            let transaction = response.results_by_transaction.first()?;
635            transaction.topic_results.first()
636        })
637        .and_then(|topic| topic.results_by_partition.first())
638        .map_or(response.error_code, |partition| {
639            partition.partition_error_code
640        })
641}
642
643impl Producer {
644    /// Add `topic`/`partition` to the current transaction with
645    /// `AddPartitionsToTxn`.
646    ///
647    /// A fence or a fatal code sets the fatal error in `txn_error`. It does not
648    /// change `txn_state`: the send path holds that lock during this call, so
649    /// the caller moves the state to [`FatalError::state`].
650    pub(crate) async fn register_transaction_partition(
651        &self,
652        topic: &str,
653        partition: i32,
654    ) -> Result<(), ProducerError> {
655        let Some(transactional_id) = &self.transactional_id else {
656            return Ok(());
657        };
658        // Kafka's `TransactionManager.maybeAddPartition`: with transaction
659        // version 2 the broker adds the partition when it gets the first
660        // transactional Produce, so no `AddPartitionsToTxn` goes out.
661        if self.transaction_v2() {
662            return Ok(());
663        }
664        let mut coordinator = self.txn_coord_client.lock().await.clone().ok_or(
665            ProducerError::InvalidTransactionState(
666                "no txn coordinator cached — did init_transactions succeed?",
667            ),
668        )?;
669        let (producer_id, producer_epoch) = *self.txn_pid_epoch.lock().await;
670        let topic = AddPartitionsToTxnTopic {
671            name: topic.to_owned(),
672            partitions: vec![partition],
673            ..Default::default()
674        };
675        let request = ClientAddPartitionsToTxn(AddPartitionsToTxnRequest {
676            v3_and_below_transactional_id: transactional_id.clone(),
677            v3_and_below_producer_id: producer_id,
678            v3_and_below_producer_epoch: producer_epoch,
679            v3_and_below_topics: vec![topic],
680            ..Default::default()
681        });
682        // Adding a partition twice has no effect, so a lost request or a
683        // retriable code is safe to retry. Kafka's `AddPartitionsToTxnHandler`
684        // does the same.
685        let mut retry = self.coordinator_retry();
686        loop {
687            let (attempt, last_error) = match coordinator.send(request.clone()).await {
688                Ok(response) => {
689                    let code = add_partitions_error_code(&response);
690                    (
691                        CoordinatorAttempt::Answered(code),
692                        ProducerError::Server(code),
693                    )
694                }
695                // Kafka's `Sender.runOnce` makes an authentication failure
696                // fatal for every pending transactional request
697                // (`TransactionManager.authenticationFailed`).
698                Err(error) if error.is_authentication_failure() => {
699                    return Err(ProducerError::Client(error));
700                }
701                Err(error) => (CoordinatorAttempt::Lost, ProducerError::Client(error)),
702            };
703            let rediscover = match txn_retry::decide_add_partitions(attempt) {
704                AddPartitionsDecision::Added => return Ok(()),
705                AddPartitionsDecision::Fenced => return Err(self.fatal_error(FatalError::Fenced)),
706                AddPartitionsDecision::Abortable(code) => {
707                    return Err(self.abortable_error(code));
708                }
709                AddPartitionsDecision::Fatal(code) => {
710                    return Err(self.fatal_error(FatalError::Server(code)));
711                }
712                AddPartitionsDecision::Retry { rediscover } => rediscover,
713            };
714            if !retry.wait().await {
715                return Err(last_error);
716            }
717            if rediscover {
718                coordinator = match self
719                    .rediscovered_txn_coordinator(transactional_id, coordinator, retry.deadline)
720                    .await
721                {
722                    Ok(coordinator) => coordinator,
723                    // The caller holds `txn_state` and moves it.
724                    Err(LookupRefusal::Abortable(code)) => return Err(self.abortable_error(code)),
725                    Err(LookupRefusal::Fatal(code)) => {
726                        return Err(self.fatal_error(FatalError::Server(code)));
727                    }
728                };
729            }
730        }
731    }
732
733    #[must_use]
734    pub fn producer_id(&self) -> i64 {
735        self.identity.id.load(Ordering::Acquire)
736    }
737
738    #[must_use]
739    pub fn producer_epoch(&self) -> i16 {
740        self.identity.epoch.load(Ordering::Acquire)
741    }
742
743    /// Give the identity that the transaction coordinator minted for this
744    /// producer's `transactional.id`.
745    ///
746    /// [`Producer::init_transactions`] sets this pair, and the broker fences a
747    /// write that carries an older one. It is the fencing token of the
748    /// transactional flow.
749    ///
750    /// This is not the pair that [`Producer::producer_id`] and
751    /// [`Producer::producer_epoch`] report. Those come from the idempotent
752    /// `InitProducerId` that the builder sends with no transactional id, and
753    /// the coordinator never advances them.
754    ///
755    /// Returns `None` before `init_transactions` runs, and for a producer that
756    /// carries no `transactional_id`.
757    pub async fn transactional_identity(&self) -> Option<(i64, i16)> {
758        minted_identity(*self.txn_pid_epoch.lock().await)
759    }
760
761    // ── Transactional API ────────────────────────────────────────────────────
762
763    /// Begin a new transaction, returning a borrowed guard that must be
764    /// finished with [`Transaction::commit`] or [`Transaction::abort`].
765    ///
766    /// The caller must call this after [`init_transactions`] has completed,
767    /// and before any transactional [`send`] call. It transitions the producer
768    /// from `Ready` to `InTransaction`.
769    ///
770    /// # Errors
771    ///
772    /// - [`ProducerError::NotTransactional`] — `transactional_id` was not set.
773    /// - [`ProducerError::InvalidTransactionState`] — the producer is not in
774    ///   the `Ready` state. For example, the caller has not yet called
775    ///   `init_transactions`, or a transaction is already in flight.
776    ///
777    /// [`init_transactions`]: Self::init_transactions
778    /// [`send`]: Self::send
779    #[tracing::instrument(
780        level = "info",
781        skip_all,
782        fields(transactional_id = self.transactional_id.as_deref()),
783        err(level = "info"),
784    )]
785    pub async fn begin_transaction(&self) -> Result<Transaction<'_>, ProducerError> {
786        let guard_generation = self.begin_transaction_state().await?;
787        Ok(Transaction {
788            producer: self,
789            finished: false,
790            guard_generation,
791        })
792    }
793
794    /// Begin a new transaction, returning an owning guard that must be
795    /// finished with [`OwnedTransaction::commit`] or
796    /// [`OwnedTransaction::abort`].
797    ///
798    /// The semantics are identical to
799    /// [`begin_transaction`](Self::begin_transaction), but the returned guard
800    /// owns an `Arc<Producer>` instead of borrowing `&self`. Use it when the
801    /// guard must survive across an owned or `'static` boundary that a borrow
802    /// cannot, for example when it is stored behind a `dyn Trait` object. It
803    /// mirrors `tokio::sync::Mutex::lock_owned`.
804    ///
805    /// # Errors
806    ///
807    /// Same as [`begin_transaction`](Self::begin_transaction).
808    #[tracing::instrument(
809        level = "info",
810        skip_all,
811        fields(transactional_id = self.transactional_id.as_deref()),
812        err(level = "info"),
813    )]
814    pub async fn begin_transaction_owned(
815        self: Arc<Self>,
816    ) -> Result<OwnedTransaction, ProducerError> {
817        let guard_generation = self.begin_transaction_state().await?;
818        Ok(OwnedTransaction {
819            producer: self,
820            finished: false,
821            guard_generation,
822        })
823    }
824
825    async fn begin_transaction_state(&self) -> Result<u64, ProducerError> {
826        if self.transactional_id.is_none() {
827            return Err(ProducerError::NotTransactional);
828        }
829        if let Some(error) = self.fatal_error_state() {
830            return Err(error);
831        }
832        if self.transaction_recovery_required() {
833            return Err(ProducerError::RecoveryRequired);
834        }
835        self.begin_transaction_locked(self.txn_state.lock().await)
836    }
837
838    /// Move `state`, the locked transaction state, into a transaction, and
839    /// give the generation of the new transaction guard.
840    fn begin_transaction_locked(
841        &self,
842        mut state: MutexGuard<'_, TxnState>,
843    ) -> Result<u64, ProducerError> {
844        match *state {
845            TxnState::Ready => {
846                *state = TxnState::InTransaction;
847                Ok(self
848                    .txn_guard_generation
849                    .fetch_add(1, Ordering::AcqRel)
850                    .wrapping_add(1))
851            }
852            _ => Err(ProducerError::InvalidTransactionState(
853                "begin_transaction must be called after init_transactions and not while another txn is in flight",
854            )),
855        }
856    }
857
858    /// Flushes the current transaction and locks it against further writes.
859    ///
860    /// The returned identity has a stable Kafka-compatible string form and can
861    /// be persisted by an external transaction coordinator. After this method
862    /// succeeds, only commit, abort, or [`complete_transaction`] may finish the
863    /// transaction.
864    ///
865    /// # Errors
866    ///
867    /// - [`ProducerError::NotTransactional`] — `transactional_id` was not set.
868    /// - [`ProducerError::InvalidTransactionState`] — 2PC is disabled or no
869    ///   transaction is currently in flight.
870    /// - [`ProducerError::FlushTimeout`] — pending records did not flush before
871    ///   the configured deadline; the transaction remains open and can retry.
872    ///
873    /// [`complete_transaction`]: Self::complete_transaction
874    pub async fn prepare_transaction(&self) -> Result<PreparedTransactionState, ProducerError> {
875        if self.transactional_id.is_none() {
876            return Err(ProducerError::NotTransactional);
877        }
878        if !self.two_phase_commit_enabled {
879            return Err(ProducerError::InvalidTransactionState(
880                "prepare_transaction requires transaction_two_phase_commit_enable=true",
881            ));
882        }
883        if let Some(error) = self.fatal_error_state() {
884            return Err(error);
885        }
886        if self.transaction_recovery_required() {
887            return Err(ProducerError::RecoveryRequired);
888        }
889
890        {
891            let mut state = self.txn_state.lock().await;
892            if *state != TxnState::InTransaction {
893                return Err(ProducerError::InvalidTransactionState(
894                    "prepare_transaction must follow begin_transaction",
895                ));
896            }
897            *state = TxnState::Preparing;
898        }
899
900        if let Err(error) = self.flush().await {
901            *self.txn_state.lock().await = TxnState::InTransaction;
902            return Err(error);
903        }
904        // Kafka's `KafkaProducer.prepareTransaction` flushes first, and then
905        // `TransactionManager.prepareTransaction` calls `maybeFailWithError`,
906        // which throws in both error states. A batch that failed during the
907        // flush has set the error by now.
908        if let Some(error) = self.transaction_error_state() {
909            *self.txn_state.lock().await = TxnState::InTransaction;
910            return Err(error);
911        }
912
913        let (producer_id, producer_epoch) = *self.txn_pid_epoch.lock().await;
914        let Ok(prepared) = PreparedTransactionState::new(producer_id, producer_epoch) else {
915            *self.txn_state.lock().await = TxnState::InTransaction;
916            return Err(ProducerError::InvalidTransactionState(
917                "transaction coordinator returned an invalid producer identity",
918            ));
919        };
920        *self.prepared_transaction_state.lock().await = Some(prepared);
921        *self.txn_state.lock().await = TxnState::Prepared;
922        self.resolve_transaction_guard();
923        Ok(prepared)
924    }
925
926    /// Completes the current prepared transaction.
927    ///
928    /// A token matching the prepared transaction commits it. A stale, empty,
929    /// or otherwise different token aborts it, so recovery never commits work
930    /// that belongs to another producer incarnation.
931    ///
932    /// # Errors
933    ///
934    /// Returns [`ProducerError::InvalidTransactionState`] unless a local
935    /// [`prepare_transaction`] or
936    /// [`init_transactions_with_keep_prepared`](Self::init_transactions_with_keep_prepared)
937    /// established a prepared transaction. Other failures are the same as
938    /// committing or aborting a normal transaction.
939    ///
940    /// [`prepare_transaction`]: Self::prepare_transaction
941    pub async fn complete_transaction(
942        &self,
943        prepared: PreparedTransactionState,
944    ) -> Result<(), ProducerError> {
945        if let Some(error) = self.fatal_error_state() {
946            return Err(error);
947        }
948        if *self.txn_state.lock().await != TxnState::Prepared {
949            return Err(ProducerError::InvalidTransactionState(
950                "complete_transaction requires a prepared transaction",
951            ));
952        }
953        let current = *self.prepared_transaction_state.lock().await;
954        self.end_transaction(current == Some(prepared)).await
955    }
956
957    /// Finish the current transaction, then send `EndTxn(committed)` to the
958    /// transaction coordinator. On success it transitions the producer from
959    /// `InTransaction` to `Ready`.
960    ///
961    /// A commit flushes every record of the transaction first, waiting for
962    /// each to be acknowledged. An abort does not: it fails every batch still
963    /// queued in the accumulators with [`ProducerError::TransactionAborted`],
964    /// sending no `Produce` for it, and waits only for batches already sent
965    /// to the wire. Kafka's `KafkaProducer.abortTransaction` does not flush
966    /// either; `Sender.maybeSendAndPollTransactionalRequest` aborts the
967    /// undrained batches instead of draining them.
968    ///
969    /// [`Transaction::commit`], [`Transaction::abort`],
970    /// [`OwnedTransaction::commit`] and [`OwnedTransaction::abort`] call it.
971    /// They are the only ways to finish a transaction opened with
972    /// `begin_transaction` or `begin_transaction_owned`.
973    ///
974    /// # Errors
975    ///
976    /// - [`ProducerError::NotTransactional`]: `transactional_id` was not set.
977    /// - [`ProducerError::InvalidTransactionState`]: not currently in a transaction.
978    /// - [`ProducerError::FencedProducer`]: broker returned `INVALID_PRODUCER_EPOCH (47)` or `PRODUCER_FENCED (90)`, now or before.
979    /// - [`ProducerError::FatalTransactionError`]: the coordinator answered with a fatal code, now or before. An abort that gets `TRANSACTION_ABORTABLE (120)` is fatal too.
980    /// - [`ProducerError::ConcurrentTransactions`]: broker still returned `CONCURRENT_TRANSACTIONS (51)` when the retry deadline ended; caller may retry.
981    /// - [`ProducerError::Server`]: an abortable code, or a retriable code that the broker still returned when the retry deadline ended.
982    /// - [`ProducerError::RecoveryRequired`]: a request was lost in transport,
983    ///   and the retries did not learn the outcome before the deadline.
984    ///
985    /// The producer sends `EndTxn` again after a retriable code, until the
986    /// producer-ID initialization retry timeout ends.
987    ///
988    /// Every error except `RecoveryRequired` means that no request changed the
989    /// transaction.
990    ///
991    /// A commit fails with the stored error when a batch of the transaction
992    /// failed, or when a coordinator answered with an abortable code. The
993    /// transaction stays open, and the application must abort it. Kafka's
994    /// `TransactionManager` keeps the same rule: a batch failure moves it to
995    /// `ABORTABLE_ERROR` (`handleFailedBatch`), and `beginCommit` then fails
996    /// in `maybeFailWithError`.
997    #[tracing::instrument(
998        level = "info",
999        skip_all,
1000        fields(committed, error_code = field::Empty),
1001        err(level = "info"),
1002    )]
1003    pub(crate) async fn end_transaction(&self, committed: bool) -> Result<(), ProducerError> {
1004        let tid = self
1005            .transactional_id
1006            .clone()
1007            .ok_or(ProducerError::NotTransactional)?;
1008
1009        // Kafka's `beginCommit` and `beginAbort` call `maybeFailWithError`,
1010        // and a fatal error fails both.
1011        if let Some(error) = self.fatal_error_state() {
1012            return Err(error);
1013        }
1014
1015        // Kafka's `KafkaProducer.commitTransaction`/`abortTransaction` move
1016        // the `TransactionManager` state (`beginCommit`/`beginAbort`) before
1017        // they wake the sender. Do the same here, before either flushing or
1018        // aborting undrained batches, so a `send` racing this call observes
1019        // the new state at once and fails instead of racing the drain below.
1020        let mut state = self.txn_state.lock().await;
1021        let previous_state = *state;
1022        if !matches!(previous_state, TxnState::InTransaction | TxnState::Prepared) {
1023            return Err(ProducerError::InvalidTransactionState(
1024                "commit/abort_transaction must follow begin_transaction",
1025            ));
1026        }
1027        *state = TxnState::CommittingOrAborting;
1028        drop(state);
1029
1030        if committed {
1031            // 1. Flush all in-flight records (block until acks).
1032            if let Err(error) = self.flush().await {
1033                *self.txn_state.lock().await = previous_state;
1034                return Err(error);
1035            }
1036
1037            // 2. A failed batch, or an abortable coordinator answer, stops a
1038            //    commit. Only an abort can clear it.
1039            if let Some(error) = self.abortable_error_state() {
1040                *self.txn_state.lock().await = previous_state;
1041                return Err(error);
1042            }
1043        } else {
1044            // 1. Fail every batch still queued in the accumulators (the
1045            //    sender has not drained it onto the wire) with
1046            //    `TransactionAborted`, and send no `Produce` for it. Then
1047            //    wait only for batches already in flight — already sent to
1048            //    the wire, awaiting ack — to complete normally. Kafka's
1049            //    `Sender.maybeSendAndPollTransactionalRequest`:
1050            //    `accumulator.abortUndrainedBatches(new TransactionAbortedException())`.
1051            if let Err(error) = self.abort_undrained_batches().await {
1052                *self.txn_state.lock().await = previous_state;
1053                return Err(error);
1054            }
1055        }
1056
1057        // 3. Retrieve the cached coordinator connection.
1058        let coord_guard = self.txn_coord_client.lock().await;
1059        let Some(coord) = coord_guard.as_ref().cloned() else {
1060            drop(coord_guard);
1061            *self.txn_state.lock().await = previous_state;
1062            return Err(ProducerError::InvalidTransactionState(
1063                "no txn coordinator cached — did init_transactions succeed?",
1064            ));
1065        };
1066        drop(coord_guard);
1067
1068        let (pid, epoch) = *self.txn_pid_epoch.lock().await;
1069
1070        // 4. Send EndTxn to the coordinator until it gives an answer that
1071        //    decides the outcome, or the retry deadline ends.
1072        let request = EndTxnRequest {
1073            transactional_id: tid,
1074            producer_id: pid,
1075            producer_epoch: epoch,
1076            committed,
1077            ..Default::default()
1078        };
1079        let end_txn = self
1080            .send_end_txn_until_decided(coord, request, committed)
1081            .await;
1082
1083        self.apply_end_txn(self.txn_state.lock().await, end_txn, previous_state)
1084            .await
1085    }
1086
1087    /// Move the transaction state for the outcome of `EndTxn`. The caller
1088    /// passes the transaction state lock, which stays held until the state
1089    /// has moved.
1090    async fn apply_end_txn(
1091        &self,
1092        mut state: MutexGuard<'_, TxnState>,
1093        end_txn: Result<(EndTxnDecision, Option<(i64, i16)>), ClientError>,
1094        previous_state: TxnState,
1095    ) -> Result<(), ProducerError> {
1096        let (decision, producer_identity) = match end_txn {
1097            Ok(answer) => answer,
1098            Err(error) => {
1099                // Kafka's `TransactionManager.authenticationFailed` makes the
1100                // producer fail. An earlier attempt may have taken effect, so
1101                // a fresh InitProducerId epoch is required before any reuse.
1102                self.require_transaction_recovery();
1103                *state = TxnState::RecoveryRequired;
1104                return Err(ProducerError::Client(error));
1105            }
1106        };
1107        match decision {
1108            EndTxnDecision::Complete => {
1109                // KIP-890 (transaction.version 2): the coordinator bumps the
1110                // producer epoch on transaction completion and returns the new
1111                // (producer_id, producer_epoch) in the EndTxn v5 response. Adopt
1112                // it so the next transaction (and its record batches, which read
1113                // this shared pair) use the un-fenced epoch. A pre-KIP-890
1114                // coordinator leaves these at -1, in which case we keep the
1115                // current pair unchanged.
1116                if let Some(identity) = producer_identity {
1117                    self.adopt_transactional_identity(identity).await;
1118                }
1119                *self.prepared_transaction_state.lock().await = None;
1120                self.txn_error.clear_abortable();
1121                *state = TxnState::Ready;
1122                self.resolve_transaction_guard();
1123                Ok(())
1124            }
1125            EndTxnDecision::Fenced => {
1126                *state = TxnState::Fenced;
1127                drop(state);
1128                Err(self.fatal_error(FatalError::Fenced))
1129            }
1130            EndTxnDecision::Fatal(code) => {
1131                *state = TxnState::FatalError;
1132                drop(state);
1133                Err(self.fatal_error(FatalError::Server(code)))
1134            }
1135            EndTxnDecision::ConcurrentTransactions => {
1136                *state = previous_state; // Caller can retry the same decision.
1137                Err(ProducerError::ConcurrentTransactions)
1138            }
1139            EndTxnDecision::Abortable(code) => {
1140                *state = previous_state;
1141                drop(state);
1142                Err(self.abortable_error(code))
1143            }
1144            EndTxnDecision::TimedOut(code) => {
1145                *state = previous_state;
1146                Err(ProducerError::Server(code))
1147            }
1148            EndTxnDecision::OutcomeUnknown | EndTxnDecision::Retry { .. } => {
1149                // A lost attempt may have taken effect. A fresh InitProducerId
1150                // epoch is required before any reuse.
1151                self.require_transaction_recovery();
1152                *state = TxnState::RecoveryRequired;
1153                Err(ProducerError::RecoveryRequired)
1154            }
1155        }
1156    }
1157
1158    /// Use a new transactional `(producer_id, producer_epoch)`.
1159    ///
1160    /// A new identity starts every partition again at sequence 0. Kafka's
1161    /// `TransactionManager` calls `resetSequenceNumbers` in the same two places,
1162    /// after `InitProducerId` and after an `EndTxn` v5 epoch bump. A broker
1163    /// that rebuilds producer state from its log sees the transaction-version-2
1164    /// end marker raise the epoch and clear the last sequence. It then rejects a
1165    /// batch at the new epoch whose sequence is not 0 with
1166    /// `OUT_OF_ORDER_SEQUENCE_NUMBER`. Every batch is flushed before either
1167    /// call, so no in-flight batch holds a sequence from the old identity.
1168    async fn adopt_transactional_identity(&self, identity: (i64, i16)) {
1169        let mut current = self.txn_pid_epoch.lock().await;
1170        if *current != identity {
1171            self.next_seq.clear();
1172        }
1173        *current = identity;
1174    }
1175
1176    /// Send one `EndTxn` request until its answer decides the outcome.
1177    ///
1178    /// This follows Kafka's `EndTxnHandler`. A transport failure, or a
1179    /// retriable code such as `CONCURRENT_TRANSACTIONS` or
1180    /// `COORDINATOR_LOAD_IN_PROGRESS`, causes a retry of the same request with
1181    /// the same producer id and epoch. The retry uses capped
1182    /// exponential backoff, and the producer-ID initialization retry timeout
1183    /// bounds it. A retry after a transport failure first finds the coordinator
1184    /// again and opens a new connection, because a broker restart leaves the
1185    /// cached connection closed.
1186    ///
1187    /// It returns the decision, and the producer identity from a `NONE` answer
1188    /// when the coordinator sent one.
1189    ///
1190    /// # Errors
1191    ///
1192    /// Returns a failed authentication with the coordinator at once. Kafka's
1193    /// `Sender.runOnce` does not retry it.
1194    async fn send_end_txn_until_decided(
1195        &self,
1196        mut coordinator: Client,
1197        request: EndTxnRequest,
1198        committed: bool,
1199    ) -> Result<(EndTxnDecision, Option<(i64, i16)>), ClientError> {
1200        let deadline = Instant::now() + self.init_retry_timeout.to_std();
1201        let max_backoff = self.retry_backoff_max.to_std();
1202        let mut backoff = self.init_retry_backoff.to_std();
1203        let mut earlier_attempt_lost = false;
1204        loop {
1205            let sent = send_in_transaction_version::<_, END_TXN_TRANSACTION_V1_MAX_VERSION>(
1206                &coordinator,
1207                request.clone(),
1208                self.transaction_v2(),
1209            )
1210            .await;
1211            let (attempt, identity) = match sent {
1212                Ok(response) => {
1213                    tracing::Span::current().record("error_code", response.error_code);
1214                    let identity = (response.producer_id >= 0)
1215                        .then_some((response.producer_id, response.producer_epoch));
1216                    (CoordinatorAttempt::Answered(response.error_code), identity)
1217                }
1218                Err(error) if error.is_authentication_failure() => return Err(error),
1219                Err(error) => {
1220                    tracing::warn!(
1221                        %error,
1222                        committed = request.committed,
1223                        "EndTxn request lost in transport; retrying to learn the outcome"
1224                    );
1225                    (CoordinatorAttempt::Lost, None)
1226                }
1227            };
1228            let decision = txn_retry::decide_end_txn(attempt, earlier_attempt_lost, committed);
1229            earlier_attempt_lost |= attempt == CoordinatorAttempt::Lost;
1230            let EndTxnDecision::Retry { rediscover } = decision else {
1231                return Ok((decision, identity));
1232            };
1233            let remaining = deadline.saturating_duration_since(Instant::now());
1234            if remaining.is_zero() {
1235                return Ok((
1236                    txn_retry::decide_end_txn_at_deadline(attempt, earlier_attempt_lost),
1237                    None,
1238                ));
1239            }
1240            time::sleep(backoff.min(remaining)).await;
1241            backoff = backoff.saturating_mul(2).min(max_backoff);
1242            if rediscover {
1243                match self
1244                    .reconnect_txn_coordinator(&request.transactional_id, deadline)
1245                    .await
1246                {
1247                    Ok(fresh) => coordinator = fresh,
1248                    Err(LookupError::Refused(refusal)) => {
1249                        let decision = txn_retry::decide_end_txn_after_refused_lookup(
1250                            refusal,
1251                            earlier_attempt_lost,
1252                            committed,
1253                        );
1254                        if !matches!(decision, EndTxnDecision::Retry { .. }) {
1255                            return Ok((decision, None));
1256                        }
1257                    }
1258                    Err(LookupError::Unresolved(error)) => {
1259                        tracing::warn!(%error, "transaction coordinator lookup failed; retrying");
1260                        // The next send on the old client fails fast and
1261                        // leads here again.
1262                        coordinator.reconnect_bootstrap().await;
1263                    }
1264                }
1265            }
1266        }
1267    }
1268
1269    /// Find the transaction coordinator again, until `deadline`, and cache a
1270    /// new connection to it.
1271    async fn reconnect_txn_coordinator(
1272        &self,
1273        tid: &str,
1274        deadline: Instant,
1275    ) -> Result<Client, LookupError> {
1276        let address = self.find_txn_coordinator(tid, deadline).await?;
1277        let coordinator = self.connect_txn_coordinator(address).await?;
1278        *self.txn_coord_client.lock().await = Some(coordinator.clone());
1279        Ok(coordinator)
1280    }
1281
1282    /// Open a client for the transaction coordinator at `address`.
1283    async fn connect_txn_coordinator(&self, address: String) -> Result<Client, ProducerError> {
1284        Ok(Client::builder()
1285            .bootstrap(address)
1286            .client_id(self.client_id.clone())
1287            .maybe_security(self.security.clone())
1288            .dns_timeout(self.dns_timeout)
1289            .metadata_recovery_strategy(self.metadata_recovery_strategy)
1290            .metadata_recovery_rebootstrap_trigger(
1291                self.metadata_recovery_rebootstrap_trigger.time(),
1292            )
1293            .client_dns_lookup(self.client_dns_lookup)
1294            .reconnect_backoff(self.reconnect_backoff)
1295            .reconnect_backoff_max(self.reconnect_backoff_max)
1296            .connections_max_idle(self.connections_max_idle)
1297            .socket_connection_setup_timeout(self.socket_connection_setup_timeout)
1298            .socket_connection_setup_timeout_max(self.socket_connection_setup_timeout_max)
1299            .dispatch_queue_capacity(self.dispatch_queue_capacity.get())
1300            .frame_max(self.frame_max.size())
1301            .send_buffer(self.socket_buffers.send)
1302            .receive_buffer(self.socket_buffers.receive)
1303            .request_timeout(self.request_timeout)
1304            .build()
1305            .await?)
1306    }
1307
1308    /// Initialize the transactional producer.
1309    ///
1310    /// The caller must call this before any transactional operation. It
1311    /// discovers the transaction coordinator with `FindCoordinator`, opens a
1312    /// dedicated connection to it, and calls `InitProducerId` to get a fenced
1313    /// `(producer_id, producer_epoch)` pair.
1314    ///
1315    /// Like Kafka's `initTransactions`, it blocks while the coordinator is not
1316    /// ready. A lookup that answers a retriable code, such as
1317    /// `COORDINATOR_NOT_AVAILABLE` from a broker that is still creating
1318    /// `__transaction_state`, or that fails in transport, goes out again. The
1319    /// producer-ID initialization retry timeout bounds the lookup and
1320    /// `InitProducerId` together.
1321    ///
1322    /// # Errors
1323    ///
1324    /// - [`ProducerError::NotTransactional`] — `transactional_id` was not set.
1325    /// - [`ProducerError::InvalidTransactionState`] — called while a
1326    ///   transaction is in flight.
1327    /// - [`ProducerError::FencedProducer`] — the broker returned
1328    ///   `INVALID_PRODUCER_EPOCH (47)` or `PRODUCER_FENCED (90)`, now or in an
1329    ///   earlier transactional operation.
1330    /// - [`ProducerError::FatalTransactionError`] — the coordinator answered
1331    ///   with a fatal code, now or in an earlier transactional operation. A
1332    ///   `FindCoordinator` answer of `TRANSACTIONAL_ID_AUTHORIZATION_FAILED`,
1333    ///   or of a code that Kafka's `FindCoordinatorHandler` does not expect, is
1334    ///   fatal. Kafka's `initTransactions` does not clear a fatal error either:
1335    ///   close the producer.
1336    /// - [`ProducerError::Server`] — an abortable code
1337    ///   (`TRANSACTIONAL_ID_AUTHORIZATION_FAILED` or
1338    ///   `CLUSTER_AUTHORIZATION_FAILED` from `InitProducerId`, or
1339    ///   `TRANSACTION_ABORTABLE`), or a retriable code at the retry deadline.
1340    ///   The call can be made again.
1341    /// - [`ProducerError::Client`] — transport-level failure, at once when it
1342    ///   repeats on every attempt, such as a failed authentication, or at the
1343    ///   retry deadline.
1344    #[tracing::instrument(
1345        level = "info",
1346        skip_all,
1347        fields(
1348            transactional_id = self.transactional_id.as_deref(),
1349            coordinator = field::Empty,
1350            producer_id = field::Empty,
1351            producer_epoch = field::Empty,
1352            error_code = field::Empty,
1353        ),
1354        err(level = "info"),
1355    )]
1356    pub async fn init_transactions(&self) -> Result<(), ProducerError> {
1357        self.init_transactions_with_keep_prepared(false).await
1358    }
1359
1360    /// Initializes the producer and optionally preserves an ongoing prepared
1361    /// transaction from an earlier producer incarnation.
1362    ///
1363    /// When `keep_prepared` is `true` and the broker reports an ongoing
1364    /// transaction, the producer enters the prepared state. The only allowed
1365    /// transaction-ending operations are commit, abort, and
1366    /// [`complete_transaction`](Self::complete_transaction). The broker returns
1367    /// both identities: the original identity becomes the persisted comparison
1368    /// token, while the newly staged identity is used for the eventual `EndTxn`.
1369    ///
1370    /// # Errors
1371    ///
1372    /// The errors are the same as [`init_transactions`](Self::init_transactions).
1373    #[tracing::instrument(
1374        level = "info",
1375        skip_all,
1376        fields(
1377            transactional_id = self.transactional_id.as_deref(),
1378            keep_prepared,
1379            coordinator = field::Empty,
1380            producer_id = field::Empty,
1381            producer_epoch = field::Empty,
1382            error_code = field::Empty,
1383        ),
1384        err(level = "info"),
1385    )]
1386    pub async fn init_transactions_with_keep_prepared(
1387        &self,
1388        keep_prepared: bool,
1389    ) -> Result<(), ProducerError> {
1390        let Some(tid) = self.transactional_id.as_deref() else {
1391            return Err(ProducerError::NotTransactional);
1392        };
1393        // Kafka's `TransactionManager.initializeTransactions` calls
1394        // `maybeFailWithError`: no transition leaves `FATAL_ERROR`, so a new
1395        // `initTransactions` does not clear it.
1396        if let Some(error) = self.fatal_error_state() {
1397            return Err(error);
1398        }
1399
1400        let previous_state = {
1401            let mut state = self.txn_state.lock().await;
1402            if !matches!(
1403                *state,
1404                TxnState::Uninitialized | TxnState::Ready | TxnState::RecoveryRequired
1405            ) && !self.transaction_recovery_required()
1406            {
1407                return Err(ProducerError::InvalidTransactionState(
1408                    "init_transactions called while a transaction is in flight",
1409                ));
1410            }
1411            let previous_state = *state;
1412            *state = TxnState::Initializing;
1413            previous_state
1414        };
1415
1416        // Kafka's `initTransactions` waits for the lookup and `InitProducerId`
1417        // together, within one `max.block.ms`.
1418        let deadline = Instant::now() + self.init_retry_timeout.to_std();
1419        let initialized = async {
1420            let coord_addr = match self.find_txn_coordinator(tid, deadline).await {
1421                Ok(address) => address,
1422                Err(error) => return Err(self.init_lookup_error(error).await),
1423            };
1424            tracing::Span::current().record("coordinator", coord_addr.as_str());
1425
1426            let coord = self.connect_txn_coordinator(coord_addr).await?;
1427
1428            self.init_producer_id_on_coordinator(
1429                tid,
1430                &InitProducerIdRequest {
1431                    transactional_id: Some(tid.to_owned()),
1432                    transaction_timeout_ms: self.transaction_timeout_ms,
1433                    enable2_pc: self.two_phase_commit_enabled,
1434                    keep_prepared_txn: keep_prepared,
1435                    ..Default::default()
1436                },
1437                coord,
1438                deadline,
1439            )
1440            .await
1441        }
1442        .await;
1443        let (coord, resp) = match initialized {
1444            Ok(initialized) => initialized,
1445            Err(error) => {
1446                // A fatal lookup answer has moved the state already.
1447                if self.fatal_error_state().is_none() {
1448                    *self.txn_state.lock().await = previous_state;
1449                }
1450                return Err(error);
1451            }
1452        };
1453
1454        tracing::Span::current().record("error_code", resp.error_code);
1455        match resp.error_code {
1456            0 => {
1457                let recovered = if keep_prepared && resp.ongoing_txn_producer_id >= 0 {
1458                    let Ok(recovered) = PreparedTransactionState::new(
1459                        resp.ongoing_txn_producer_id,
1460                        resp.ongoing_txn_producer_epoch,
1461                    ) else {
1462                        *self.txn_state.lock().await = previous_state;
1463                        return Err(ProducerError::InvalidTransactionState(
1464                            "transaction coordinator returned an invalid ongoing identity",
1465                        ));
1466                    };
1467                    Some(recovered)
1468                } else {
1469                    None
1470                };
1471                tracing::Span::current().record("producer_id", resp.producer_id);
1472                tracing::Span::current().record("producer_epoch", resp.producer_epoch);
1473                self.adopt_transactional_identity((resp.producer_id, resp.producer_epoch))
1474                    .await;
1475                self.txn_error.clear_abortable();
1476                // Kafka's `KafkaProducer.initTransactions` reads the finalized
1477                // features after `InitProducerId`
1478                // (`maybeUpdateTransactionV2Enabled(true)`).
1479                self.transaction_v2.store(
1480                    transaction_v2_enabled(&coord.finalized_features()),
1481                    Ordering::Release,
1482                );
1483                *self.txn_coord_client.lock().await = Some(coord);
1484                *self.prepared_transaction_state.lock().await = recovered;
1485                *self.txn_state.lock().await = if recovered.is_some() {
1486                    TxnState::Prepared
1487                } else {
1488                    TxnState::Ready
1489                };
1490                self.txn_recovery_required.store(false, Ordering::Release);
1491                self.resolve_transaction_guard();
1492                Ok(())
1493            }
1494            code => match txn_retry::decide_init_producer_id(CoordinatorAttempt::Answered(code)) {
1495                TxnRequestDecision::Fenced => Err(self.fence_transaction().await),
1496                TxnRequestDecision::Fatal(code) => Err(self.fatal_transaction(code).await),
1497                // An abortable code, or a retriable code at the retry
1498                // deadline, leaves the producer as it was.
1499                TxnRequestDecision::Done
1500                | TxnRequestDecision::Abortable(_)
1501                | TxnRequestDecision::Retry { .. } => {
1502                    *self.txn_state.lock().await = previous_state;
1503                    Err(ProducerError::Server(code))
1504                }
1505            },
1506        }
1507    }
1508
1509    /// Send `InitProducerId` to the transaction coordinator until it answers,
1510    /// or until the retry deadline ends. It gives back the coordinator that
1511    /// answered, which is a new connection after a coordinator move.
1512    ///
1513    /// The retries follow Kafka's `InitProducerIdHandler`. A transport loss,
1514    /// `COORDINATOR_NOT_AVAILABLE` and `NOT_COORDINATOR` find the coordinator
1515    /// again. Every other retriable code sends the request again on the same
1516    /// connection. The caller reads the error code of the answer. `deadline`
1517    /// bounds the retries and the lookups.
1518    async fn init_producer_id_on_coordinator(
1519        &self,
1520        transactional_id: &str,
1521        request: &InitProducerIdRequest,
1522        coordinator: Client,
1523        deadline: Instant,
1524    ) -> Result<(Client, InitProducerIdResponse), ProducerError> {
1525        let mut coordinator = coordinator;
1526        let mut retry = self.coordinator_retry_until(deadline);
1527        loop {
1528            let (attempt, last_outcome) = match send_init_producer_id(&coordinator, request).await {
1529                Ok(response) => (
1530                    CoordinatorAttempt::Answered(response.error_code),
1531                    Ok(response),
1532                ),
1533                Err(error @ ClientError::Disconnected) => (CoordinatorAttempt::Lost, Err(error)),
1534                Err(error) => return Err(ProducerError::Client(error)),
1535            };
1536            let TxnRequestDecision::Retry { rediscover } =
1537                txn_retry::decide_init_producer_id(attempt)
1538            else {
1539                return last_outcome
1540                    .map(|response| (coordinator, response))
1541                    .map_err(ProducerError::Client);
1542            };
1543            if !retry.wait().await {
1544                return last_outcome
1545                    .map(|response| (coordinator, response))
1546                    .map_err(ProducerError::Client);
1547            }
1548            if rediscover {
1549                coordinator = match self
1550                    .rediscovered_txn_coordinator(transactional_id, coordinator, deadline)
1551                    .await
1552                {
1553                    Ok(coordinator) => coordinator,
1554                    Err(refusal) => {
1555                        return Err(self.init_lookup_error(LookupError::Refused(refusal)).await);
1556                    }
1557                };
1558            }
1559        }
1560    }
1561
1562    /// Discover the transaction coordinator for `tid` with `FindCoordinator`,
1563    /// and retry until `deadline`. See [`Self::find_coordinator`].
1564    #[tracing::instrument(level = "debug", skip_all, fields(transactional_id = %tid), err(level = "debug"))]
1565    async fn find_txn_coordinator(
1566        &self,
1567        tid: &str,
1568        deadline: Instant,
1569    ) -> Result<String, LookupError> {
1570        self.find_coordinator(tid, CoordinatorKeyType::Transaction, deadline)
1571            .await
1572    }
1573
1574    /// Send `FindCoordinator` for `key` until the broker names the
1575    /// coordinator, or until `deadline`, and give the `host:port` address.
1576    ///
1577    /// The retries follow Kafka's `FindCoordinatorHandler`: a disconnect and
1578    /// every retriable code, such as `COORDINATOR_LOAD_IN_PROGRESS` and
1579    /// `COORDINATOR_NOT_AVAILABLE` from a broker that is still creating
1580    /// `__transaction_state`, send the lookup again after a backoff. The
1581    /// request handles both the legacy top-level response, versions 0 to 3,
1582    /// and the `coordinators` array that version 4 introduced.
1583    async fn find_coordinator(
1584        &self,
1585        key: &str,
1586        key_type: CoordinatorKeyType,
1587        deadline: Instant,
1588    ) -> Result<String, LookupError> {
1589        let mut retry = self.coordinator_retry_until(deadline);
1590        loop {
1591            let (attempt, address, last_error) = match self
1592                .client
1593                .send(build_find_coordinator(key, key_type))
1594                .await
1595            {
1596                Ok(response) => {
1597                    let (code, address) = coordinator_answer(&response);
1598                    (
1599                        CoordinatorAttempt::Answered(code),
1600                        address,
1601                        ProducerError::Server(code),
1602                    )
1603                }
1604                Err(error) if lookup_may_succeed_later(&error) => (
1605                    CoordinatorAttempt::Lost,
1606                    String::new(),
1607                    ProducerError::Client(error),
1608                ),
1609                Err(error) => {
1610                    return Err(LookupError::Unresolved(ProducerError::Client(error)));
1611                }
1612            };
1613            match txn_retry::decide_find_coordinator(attempt) {
1614                FindCoordinatorDecision::Found => return Ok(address),
1615                FindCoordinatorDecision::Retry => {}
1616                FindCoordinatorDecision::Refused(refusal) => {
1617                    tracing::warn!(
1618                        ?key_type,
1619                        key,
1620                        ?refusal,
1621                        "could not find a coordinator; the broker refused the lookup"
1622                    );
1623                    return Err(LookupError::Refused(refusal));
1624                }
1625            }
1626            if attempt == CoordinatorAttempt::Lost {
1627                // The same failure can have closed the bootstrap connection.
1628                self.client.reconnect_bootstrap().await;
1629            }
1630            if !retry.wait().await {
1631                return Err(LookupError::Unresolved(last_error));
1632            }
1633        }
1634    }
1635
1636    /// Give the error of a refused lookup inside a transaction, and move the
1637    /// transaction as Kafka's `FindCoordinatorHandler` does: `abortableError`
1638    /// or `fatalError`.
1639    ///
1640    /// The caller must not hold `txn_state`.
1641    async fn refused_lookup(&self, refusal: LookupRefusal) -> ProducerError {
1642        match refusal {
1643            LookupRefusal::Abortable(code) => self.abortable_error(code),
1644            LookupRefusal::Fatal(code) => self.fatal_transaction(code).await,
1645        }
1646    }
1647
1648    /// Give the error of a lookup that ended `init_transactions`.
1649    ///
1650    /// A fatal code moves the producer to the fatal error state. An abortable
1651    /// code leaves the producer as it was, as an abortable `InitProducerId`
1652    /// answer does, so the call can be made again.
1653    async fn init_lookup_error(&self, error: LookupError) -> ProducerError {
1654        match error {
1655            LookupError::Refused(LookupRefusal::Fatal(code)) => self.fatal_transaction(code).await,
1656            LookupError::Refused(LookupRefusal::Abortable(code)) => ProducerError::Server(code),
1657            LookupError::Unresolved(error) => error,
1658        }
1659    }
1660
1661    /// Enroll a consumer group's offsets in the current transaction, and fence
1662    /// zombie producers with the supplied [`ConsumerGroupMetadata`], as
1663    /// KIP-447 defines.
1664    ///
1665    /// It first waits for metadata that names the topics of `offsets`, as
1666    /// Kafka's producer does, so that `TxnOffsetCommit` can name each topic
1667    /// by its id. Then it does two broker round-trips:
1668    ///
1669    /// 1. `AddOffsetsToTxn` to the transaction coordinator. This registers the
1670    ///    group offset commit as part of the ongoing transaction.
1671    /// 2. `TxnOffsetCommit` to the group coordinator. This commits the actual
1672    ///    offsets transactionally. It carries the generation, member and
1673    ///    instance of `group_meta`, so the coordinator can fence stale
1674    ///    producers.
1675    ///
1676    /// Each request goes out again after a transport loss and after a
1677    /// retriable code, until the producer-ID initialization retry timeout
1678    /// ends. A code that shows the coordinator moved also finds the
1679    /// coordinator again. The rules follow Kafka's `AddOffsetsToTxnHandler`
1680    /// and `TxnOffsetCommitHandler`.
1681    ///
1682    /// # Errors
1683    ///
1684    /// - [`ProducerError::NotTransactional`] — `transactional_id` was not set.
1685    /// - [`ProducerError::InvalidTransactionState`] — there is no cached
1686    ///   transaction coordinator. Call [`init_transactions`] first.
1687    /// - [`ProducerError::FencedProducer`] — the coordinator answered
1688    ///   `INVALID_PRODUCER_EPOCH (47)` or `PRODUCER_FENCED (90)`.
1689    /// - [`ProducerError::Server`] — any other broker error code. A code that
1690    ///   Kafka calls abortable also makes every later commit fail, so the
1691    ///   application must abort the transaction.
1692    /// - [`ProducerError::MetadataTimeout`] — no metadata response named a
1693    ///   topic of `offsets` within `max_block`. Kafka's
1694    ///   `sendOffsetsToTransaction` throws `TimeoutException`.
1695    /// - [`ProducerError::Client`] — transport-level failure.
1696    ///
1697    /// [`init_transactions`]: Self::init_transactions
1698    #[tracing::instrument(
1699        level = "info",
1700        skip_all,
1701        fields(
1702            transactional_id = self.transactional_id.as_deref(),
1703            group_id = %group_meta.group_id,
1704            generation_id = group_meta.generation_id,
1705            offset_count = field::Empty,
1706        ),
1707        err(level = "info"),
1708    )]
1709    pub async fn send_offsets_to_transaction(
1710        &self,
1711        offsets: impl IntoIterator<Item = ((String, i32), i64)>,
1712        group_meta: &ConsumerGroupMetadata,
1713    ) -> Result<(), ProducerError> {
1714        let tid = self
1715            .transactional_id
1716            .as_deref()
1717            .ok_or(ProducerError::NotTransactional)?
1718            .to_owned();
1719        if let Some(error) = self.fatal_error_state() {
1720            return Err(error);
1721        }
1722        let state = *self.txn_state.lock().await;
1723        match state {
1724            TxnState::InTransaction => {}
1725            TxnState::Preparing | TxnState::Prepared => {
1726                return Err(ProducerError::InvalidTransactionState(
1727                    "send_offsets_to_transaction is not allowed after prepare_transaction",
1728                ));
1729            }
1730            _ => {
1731                return Err(ProducerError::InvalidTransactionState(
1732                    "cannot send offsets to transaction while no transaction is in progress",
1733                ));
1734            }
1735        }
1736        let offsets_vec: Vec<_> = offsets.into_iter().collect();
1737        tracing::Span::current().record("offset_count", offsets_vec.len());
1738        if let Some(error) = self.abortable_error_state() {
1739            return Err(error);
1740        }
1741
1742        // Kafka's `KafkaProducer.sendOffsetsToTransaction` waits for the
1743        // metadata of the topics first (`awaitTopicMetadata`), so the request
1744        // can name each topic by its id.
1745        let topic_ids = self.offset_topic_ids(&offsets_vec).await?;
1746        let (pid, epoch) = *self.txn_pid_epoch.lock().await;
1747
1748        // 1. AddOffsetsToTxn → transaction coordinator. Kafka's
1749        //    `TransactionManager.sendOffsetsToTransaction` skips it with
1750        //    transaction version 2: the group coordinator adds the offsets
1751        //    partition when it gets `TxnOffsetCommit` v5 or later.
1752        if !self.transaction_v2() {
1753            self.add_offsets_to_txn(&tid, pid, epoch, &group_meta.group_id)
1754                .await?;
1755        }
1756
1757        // 2. FindCoordinator(group_id, key_type=0 GROUP), then TxnOffsetCommit
1758        //    → group coordinator, carrying the consumer group metadata
1759        //    (generation id / member id / instance id) so the coordinator can
1760        //    fence zombie producers through the group's own state rather than
1761        //    requiring one producer per input partition (KIP-447).
1762        let topics = build_topics_payload(&offsets_vec, &topic_ids);
1763        self.txn_offset_commit(&tid, pid, epoch, group_meta, topics)
1764            .await
1765    }
1766
1767    /// Wait for the metadata of the topics of `offsets`, and return the id of
1768    /// each topic, or [`Uuid::ZERO`] for a topic that the metadata does not
1769    /// hold. See [`MetadataWait::topic_ids`].
1770    async fn offset_topic_ids(
1771        &self,
1772        offsets: &[((String, i32), i64)],
1773    ) -> Result<HashMap<String, Uuid>, ProducerError> {
1774        let mut topics: Vec<String> = Vec::new();
1775        for ((topic, _), _) in offsets {
1776            if !topics.contains(topic) {
1777                topics.push(topic.clone());
1778            }
1779        }
1780        // Kafka's `awaitTopicMetadata` calls `ProducerMetadata.add` for the
1781        // topics, so the periodic and error refreshes name them.
1782        for topic in &topics {
1783            self.client.metadata_topics().add(topic);
1784        }
1785        self.metadata_wait()
1786            .topic_ids(&topics, |topics| self.refresh_producer_metadata(topics))
1787            .await
1788    }
1789
1790    /// Send `AddOffsetsToTxn` to the transaction coordinator until it answers,
1791    /// or until the retry deadline ends.
1792    ///
1793    /// The retries follow Kafka's `AddOffsetsToTxnHandler`. A transport loss,
1794    /// `COORDINATOR_NOT_AVAILABLE` and `NOT_COORDINATOR` find the coordinator
1795    /// again. Every other retriable code sends the request again on the same
1796    /// connection. The coordinator adds the offsets topic partition to the
1797    /// transaction once, so a retry is safe.
1798    async fn add_offsets_to_txn(
1799        &self,
1800        transactional_id: &str,
1801        producer_id: i64,
1802        producer_epoch: i16,
1803        group_id: &str,
1804    ) -> Result<(), ProducerError> {
1805        let mut coordinator = self.txn_coord_client.lock().await.clone().ok_or(
1806            ProducerError::InvalidTransactionState(
1807                "no txn coordinator cached — did init_transactions succeed?",
1808            ),
1809        )?;
1810        let request = AddOffsetsToTxnRequest {
1811            transactional_id: transactional_id.to_owned(),
1812            producer_id,
1813            producer_epoch,
1814            group_id: group_id.to_owned(),
1815            ..Default::default()
1816        };
1817        let mut retry = self.coordinator_retry();
1818        loop {
1819            let (attempt, last_error) = match coordinator.send(request.clone()).await {
1820                Ok(response) => (
1821                    CoordinatorAttempt::Answered(response.error_code),
1822                    ProducerError::Server(response.error_code),
1823                ),
1824                Err(error) if request_may_have_reached_the_broker(&error) => {
1825                    (CoordinatorAttempt::Lost, ProducerError::Client(error))
1826                }
1827                Err(error) => return Err(ProducerError::Client(error)),
1828            };
1829            let rediscover = match txn_retry::decide_add_offsets_to_txn(attempt) {
1830                TxnRequestDecision::Done => return Ok(()),
1831                TxnRequestDecision::Fenced => return Err(self.fence_transaction().await),
1832                TxnRequestDecision::Abortable(code) => return Err(self.abortable_error(code)),
1833                TxnRequestDecision::Fatal(code) => return Err(self.fatal_transaction(code).await),
1834                TxnRequestDecision::Retry { rediscover } => rediscover,
1835            };
1836            if !retry.wait().await {
1837                return Err(last_error);
1838            }
1839            if rediscover {
1840                coordinator = match self
1841                    .rediscovered_txn_coordinator(transactional_id, coordinator, retry.deadline)
1842                    .await
1843                {
1844                    Ok(coordinator) => coordinator,
1845                    Err(refusal) => return Err(self.refused_lookup(refusal).await),
1846                };
1847            }
1848        }
1849    }
1850
1851    /// Send `TxnOffsetCommit` to the group coordinator until it answers, or
1852    /// until the retry deadline ends.
1853    ///
1854    /// The retries follow Kafka's `TxnOffsetCommitHandler`. A transport loss,
1855    /// `COORDINATOR_NOT_AVAILABLE`, `NOT_COORDINATOR` and `REQUEST_TIMED_OUT`
1856    /// find the group coordinator again. Every other retriable code sends the
1857    /// request again on the same connection. Kafka sends only the partitions
1858    /// that failed with a retriable code; this producer sends the whole
1859    /// request again, and the coordinator writes the same offsets.
1860    ///
1861    /// The request goes out at v6 (KIP-1319), which names each topic by id,
1862    /// when every topic of `topics` has an id and the producer follows
1863    /// transaction version 2. Otherwise it names each topic at v5 or lower.
1864    /// `UNKNOWN_TOPIC_ID` is retriable, so the same request goes out again, as
1865    /// Kafka's handler re-enqueues it with the ids that it resolved at first.
1866    async fn txn_offset_commit(
1867        &self,
1868        transactional_id: &str,
1869        producer_id: i64,
1870        producer_epoch: i16,
1871        group_meta: &ConsumerGroupMetadata,
1872        topics: Vec<TxnOffsetCommitRequestTopic>,
1873    ) -> Result<(), ProducerError> {
1874        let cap = TxnOffsetCommitCap::of(&topics, self.transaction_v2());
1875        let request = TxnOffsetCommitRequest {
1876            transactional_id: transactional_id.to_owned(),
1877            producer_id,
1878            producer_epoch,
1879            group_id: group_meta.group_id.clone(),
1880            generation_id_or_member_epoch: group_meta.generation_id,
1881            member_id: group_meta.member_id.clone(),
1882            group_instance_id: group_meta.group_instance_id.clone(),
1883            topics,
1884            ..Default::default()
1885        };
1886        let mut retry = self.coordinator_retry();
1887        let mut group_client = match self
1888            .connect_group_coordinator(&group_meta.group_id, retry.deadline)
1889            .await
1890        {
1891            Ok(client) => client,
1892            Err(LookupError::Refused(refusal)) => return Err(self.refused_lookup(refusal).await),
1893            Err(LookupError::Unresolved(error)) => return Err(error),
1894        };
1895        loop {
1896            let (attempt, last_error) = match cap.send(&group_client, request.clone()).await {
1897                Ok(response) => {
1898                    let code = txn_offset_commit_error_code(&response);
1899                    (
1900                        CoordinatorAttempt::Answered(code),
1901                        ProducerError::Server(code),
1902                    )
1903                }
1904                Err(error) if request_may_have_reached_the_broker(&error) => {
1905                    (CoordinatorAttempt::Lost, ProducerError::Client(error))
1906                }
1907                Err(error) => return Err(ProducerError::Client(error)),
1908            };
1909            let rediscover = match txn_retry::decide_txn_offset_commit(attempt) {
1910                TxnRequestDecision::Done => return Ok(()),
1911                TxnRequestDecision::Fenced => return Err(self.fence_transaction().await),
1912                TxnRequestDecision::Abortable(code) => return Err(self.abortable_error(code)),
1913                TxnRequestDecision::Fatal(code) => return Err(self.fatal_transaction(code).await),
1914                TxnRequestDecision::Retry { rediscover } => rediscover,
1915            };
1916            if !retry.wait().await {
1917                return Err(last_error);
1918            }
1919            if rediscover {
1920                match self
1921                    .connect_group_coordinator(&group_meta.group_id, retry.deadline)
1922                    .await
1923                {
1924                    Ok(fresh) => group_client = fresh,
1925                    Err(LookupError::Refused(refusal)) => {
1926                        return Err(self.refused_lookup(refusal).await);
1927                    }
1928                    Err(LookupError::Unresolved(error)) => {
1929                        tracing::warn!(%error, "group coordinator lookup failed; retrying");
1930                        // `FindCoordinator` goes out on the bootstrap
1931                        // connection, which the same restart can have closed.
1932                        self.client.reconnect_bootstrap().await;
1933                    }
1934                }
1935            }
1936        }
1937    }
1938
1939    /// Find the group coordinator of `group_id`, until `deadline`, and
1940    /// connect to it.
1941    async fn connect_group_coordinator(
1942        &self,
1943        group_id: &str,
1944        deadline: Instant,
1945    ) -> Result<Client, LookupError> {
1946        let group_addr = self.find_group_coordinator(group_id, deadline).await?;
1947        Client::builder()
1948            .bootstrap(group_addr)
1949            .client_id(self.client_id.clone())
1950            .maybe_security(self.security.clone())
1951            .dns_timeout(self.dns_timeout)
1952            .metadata_recovery_strategy(self.metadata_recovery_strategy)
1953            .metadata_recovery_rebootstrap_trigger(
1954                self.metadata_recovery_rebootstrap_trigger.time(),
1955            )
1956            .client_dns_lookup(self.client_dns_lookup)
1957            .reconnect_backoff(self.reconnect_backoff)
1958            .reconnect_backoff_max(self.reconnect_backoff_max)
1959            .connections_max_idle(self.connections_max_idle)
1960            .socket_connection_setup_timeout(self.socket_connection_setup_timeout)
1961            .socket_connection_setup_timeout_max(self.socket_connection_setup_timeout_max)
1962            .dispatch_queue_capacity(self.dispatch_queue_capacity.get())
1963            .frame_max(self.frame_max.size())
1964            .request_timeout(self.request_timeout)
1965            .send_buffer(self.socket_buffers.send)
1966            .receive_buffer(self.socket_buffers.receive)
1967            .build()
1968            .await
1969            .map_err(|error| LookupError::Unresolved(error.into()))
1970    }
1971
1972    /// Discover the group coordinator for `group_id` with `FindCoordinator`
1973    /// and `key_type = 0`, which is GROUP, and retry until `deadline`.
1974    ///
1975    /// This mirrors [`find_txn_coordinator`], but it looks up the group
1976    /// coordinator rather than the transaction coordinator.
1977    ///
1978    /// [`find_txn_coordinator`]: Self::find_txn_coordinator
1979    #[tracing::instrument(level = "debug", skip_all, fields(group_id = %group_id), err(level = "debug"))]
1980    async fn find_group_coordinator(
1981        &self,
1982        group_id: &str,
1983        deadline: Instant,
1984    ) -> Result<String, LookupError> {
1985        self.find_coordinator(group_id, CoordinatorKeyType::Group, deadline)
1986            .await
1987    }
1988
1989    // ── Internal lifecycle ───────────────────────────────────────────────────
1990
1991    /// The retry limits of a coordinator request. The producer-ID
1992    /// initialization retry timeout limits every such loop.
1993    fn coordinator_retry(&self) -> CoordinatorRetry {
1994        self.coordinator_retry_until(Instant::now() + self.init_retry_timeout.to_std())
1995    }
1996
1997    /// The retry limits of a coordinator request that ends at `deadline`,
1998    /// which the operation that sends it set.
1999    fn coordinator_retry_until(&self, deadline: Instant) -> CoordinatorRetry {
2000        CoordinatorRetry {
2001            deadline,
2002            backoff: self.init_retry_backoff.to_std(),
2003            max_backoff: self.retry_backoff_max.to_std(),
2004        }
2005    }
2006
2007    /// Find the transaction coordinator again, until `deadline`, and connect
2008    /// to it. It gives `current` back when the lookup does not resolve, after
2009    /// it reconnects the bootstrap connection, and it gives the refusal when
2010    /// the broker refuses the lookup.
2011    async fn rediscovered_txn_coordinator(
2012        &self,
2013        transactional_id: &str,
2014        current: Client,
2015        deadline: Instant,
2016    ) -> Result<Client, LookupRefusal> {
2017        match self
2018            .reconnect_txn_coordinator(transactional_id, deadline)
2019            .await
2020        {
2021            Ok(fresh) => Ok(fresh),
2022            Err(LookupError::Refused(refusal)) => Err(refusal),
2023            Err(LookupError::Unresolved(error)) => {
2024                tracing::warn!(%error, "transaction coordinator lookup failed; retrying");
2025                current.reconnect_bootstrap().await;
2026                Ok(current)
2027            }
2028        }
2029    }
2030
2031    /// Mark the transaction fenced, and give the error to report. A newer
2032    /// epoch owns the transactional id, so no request of this producer can
2033    /// change the transaction. Kafka's `TransactionManager.fatalError` leaves
2034    /// the producer in the same place.
2035    async fn fence_transaction(&self) -> ProducerError {
2036        *self.txn_state.lock().await = TxnState::Fenced;
2037        self.fatal_error(FatalError::Fenced)
2038    }
2039
2040    /// Move the transaction to the fatal error state for `code`, and give the
2041    /// error to report. Kafka's `TransactionManager.fatalError` does the same.
2042    async fn fatal_transaction(&self, code: i16) -> ProducerError {
2043        *self.txn_state.lock().await = TxnState::FatalError;
2044        self.fatal_error(FatalError::Server(code))
2045    }
2046
2047    /// Record `error` as the fatal error of the transaction, and give the
2048    /// error to report. The caller moves `txn_state`.
2049    ///
2050    /// The open transaction guard is resolved, because no request of this
2051    /// producer can change the transaction again: a dropped guard must not
2052    /// ask for recovery. The sender fails every transactional batch that it
2053    /// has not sent, as Kafka's `Sender.runOnce` calls `maybeAbortBatches`
2054    /// in `FATAL_ERROR`, so the sender is woken.
2055    fn fatal_error(&self, error: FatalError) -> ProducerError {
2056        self.txn_error.set_fatal(error);
2057        self.resolve_transaction_guard();
2058        tracing::error!(
2059            ?error,
2060            "the transactional producer is in a fatal error state; close it"
2061        );
2062        let _ = self.wake_tx.try_send(DrainIntent::Ready);
2063        error.error()
2064    }
2065
2066    /// The fatal error of the transaction, as the error that every
2067    /// transactional operation reports.
2068    fn fatal_error_state(&self) -> Option<ProducerError> {
2069        self.txn_error.fatal().map(FatalError::error)
2070    }
2071
2072    /// The error that `maybeFailWithError` throws in Kafka's
2073    /// `TransactionManager`: the fatal error, or else the abortable error.
2074    fn transaction_error_state(&self) -> Option<ProducerError> {
2075        self.fatal_error_state()
2076            .or_else(|| self.abortable_error_state())
2077    }
2078
2079    /// Record `code` as the error that only an abort can clear, and give the
2080    /// error to report. Kafka's `TransactionManager.abortableError` does the
2081    /// same.
2082    fn abortable_error(&self, code: i16) -> ProducerError {
2083        self.txn_error.set_abortable(code);
2084        tracing::warn!(
2085            error_code = code,
2086            "the transaction can no longer commit; abort it"
2087        );
2088        ProducerError::Server(code)
2089    }
2090
2091    /// The error that a commit reports while the transaction is abort-only.
2092    fn abortable_error_state(&self) -> Option<ProducerError> {
2093        self.txn_error.abortable().map(|error| match error {
2094            AbortableError::Server(code) => ProducerError::Server(code),
2095            AbortableError::Timeout => ProducerError::SendTimeout,
2096        })
2097    }
2098
2099    /// Tell if the producer follows transaction version 2.
2100    fn transaction_v2(&self) -> bool {
2101        self.transaction_v2.load(Ordering::Acquire)
2102    }
2103
2104    pub(crate) fn is_active(&self) -> Result<(), ProducerError> {
2105        match self.state.load(Ordering::Acquire) {
2106            STATE_ACTIVE => Ok(()),
2107            STATE_FENCED => Err(ProducerError::FencedProducer),
2108            _ => Err(ProducerError::Closed),
2109        }
2110    }
2111
2112    pub(crate) fn require_transaction_recovery(&self) {
2113        if !self.txn_recovery_required.swap(true, Ordering::AcqRel) {
2114            self.txn_recovery_generation.fetch_add(1, Ordering::AcqRel);
2115        }
2116        let _ = self.wake_tx.try_send(DrainIntent::Ready);
2117    }
2118
2119    fn transaction_recovery_required(&self) -> bool {
2120        self.txn_recovery_required.load(Ordering::Acquire)
2121    }
2122
2123    pub(crate) fn abandon_transaction_guard(&self, generation: u64) {
2124        if self
2125            .txn_guard_generation
2126            .compare_exchange(
2127                generation,
2128                generation.wrapping_add(1),
2129                Ordering::AcqRel,
2130                Ordering::Acquire,
2131            )
2132            .is_ok()
2133        {
2134            self.require_transaction_recovery();
2135        }
2136    }
2137
2138    fn resolve_transaction_guard(&self) {
2139        let generation = self.txn_guard_generation.load(Ordering::Acquire);
2140        if generation % 2 == 1 {
2141            let _ = self.txn_guard_generation.compare_exchange(
2142                generation,
2143                generation.wrapping_add(1),
2144                Ordering::AcqRel,
2145                Ordering::Acquire,
2146            );
2147        }
2148    }
2149
2150    #[allow(dead_code)] // wired by sender on INVALID_PRODUCER_EPOCH; kept for symmetry
2151    pub(crate) fn fence(&self) {
2152        let _ = self.state.compare_exchange(
2153            STATE_ACTIVE,
2154            STATE_FENCED,
2155            Ordering::AcqRel,
2156            Ordering::Acquire,
2157        );
2158    }
2159
2160    /// Send a record and wait for its delivery result.
2161    ///
2162    /// Use [`Self::enqueue`] to queue multiple records before waiting for
2163    /// delivery. Once queued, dropping this future does not cancel the record.
2164    ///
2165    /// # Errors
2166    ///
2167    /// Returns enqueue or delivery failures as [`ProducerError`].
2168    pub async fn send(&self, record: ProducerRecord) -> Result<RecordMetadata, ProducerError> {
2169        self.enqueue(record).await?.await
2170    }
2171
2172    /// Enqueue a record and return a future that resolves when the broker
2173    /// acks, or when the producer fences or closes.
2174    ///
2175    /// A record with a negative partition or timestamp fails at once with
2176    /// [`ProducerError::InvalidPartition`] or
2177    /// [`ProducerError::InvalidTimestamp`], and the producer sends nothing.
2178    ///
2179    /// A transactional producer fails the record at once, and sends nothing,
2180    /// when its transaction holds an error: the abortable error until the
2181    /// application aborts, and the fatal error for good. Kafka's
2182    /// `KafkaProducer.doSend` fails the same way in
2183    /// `TransactionManager.maybeAddPartition`.
2184    ///
2185    /// Waits for metadata and buffer space for at most `max_block` before
2186    /// queueing. Dropping the returned handle does not cancel delivery.
2187    ///
2188    /// # Errors
2189    ///
2190    /// Returns validation, producer state, metadata, or buffer allocation
2191    /// failures before queueing. Delivery errors are returned by the handle.
2192    pub async fn enqueue(&self, record: ProducerRecord) -> Result<DeliveryHandle, ProducerError> {
2193        let span = tracing::debug_span!(
2194            "producer.send",
2195            topic = %record.topic,
2196            partition = field::Empty,
2197        );
2198        self.enqueue_inner(record).instrument(span).await
2199    }
2200
2201    async fn enqueue_inner(&self, record: ProducerRecord) -> Result<DeliveryHandle, ProducerError> {
2202        // Kafka checks the record when the application creates it, before
2203        // `send` makes any other check.
2204        record.validate()?;
2205        self.is_active()?;
2206        if self.transactional_id.is_some() && self.transaction_recovery_required() {
2207            return Err(ProducerError::RecoveryRequired);
2208        }
2209
2210        // Kafka's `KafkaProducer.doSend` calls `throwIfInPreparedState` before
2211        // `waitOnMetadata`, so a send after `prepare_transaction` does not
2212        // wait for metadata. The check before the append covers a
2213        // `prepare_transaction` that starts during the wait. It uses
2214        // `append_transaction_generation`, not `transaction_generation`
2215        // directly, so a fatal or abortable transaction error reports itself
2216        // here rather than the generic "no transaction in progress" state
2217        // error, matching `TransactionManager.maybeFailWithError` in Kafka's
2218        // `maybeAddPartition`.
2219        if self.transactional_id.is_some() {
2220            let state = *self.txn_state.lock().await;
2221            self.append_transaction_generation(Some(state))?;
2222        }
2223
2224        // Produce v13 carries only the `topic_id` on the wire, so the cache
2225        // must hold the topic also when the caller names the partition.
2226        let metadata_started = Instant::now();
2227        let partition_count = self
2228            .partition_count(&record.topic, record.partition)
2229            .await?;
2230        // A record that names no partition goes to a custom partitioner when
2231        // one is set (Kafka's `KafkaProducer.partition`), or otherwise to its
2232        // keyed partition, or to the sticky partition when it has neither.
2233        let fixed_partition = match record.partition {
2234            Some(partition) => Some(partition),
2235            None => match &self.custom_partitioner {
2236                Some(partitioner) => {
2237                    let (available, all) = self.partition_infos(&record.topic, partition_count);
2238                    let picked = partitioner.partition(
2239                        &record.topic,
2240                        record.key.as_deref(),
2241                        record.value.as_deref(),
2242                        &available,
2243                        &all,
2244                    );
2245                    if picked < 0 {
2246                        return Err(ProducerError::InvalidPartitionerResult(picked));
2247                    }
2248                    Some(picked)
2249                }
2250                None => self
2251                    .partitioner
2252                    .keyed_partition(record.key.as_deref(), partition_count),
2253            },
2254        };
2255        let has_leader = |partition: i32| {
2256            self.partition_leaders
2257                .get(&(record.topic.clone(), partition))
2258                .is_some_and(|leader| *leader >= 0)
2259        };
2260        let partitions = TopicPartitions {
2261            count: partition_count,
2262            has_leader: &has_leader,
2263        };
2264        // Kafka's `KafkaProducer.doSend` gives the buffer the part of
2265        // `max.block.ms` that the metadata wait left, in whole milliseconds.
2266        let waited_on_metadata = Duration::from_millis(
2267            u64::try_from(metadata_started.elapsed().as_millis()).unwrap_or(u64::MAX),
2268        );
2269        let remaining_block = self.max_block.saturating_sub(waited_on_metadata);
2270
2271        // Kafka's `KafkaProducer.ensureValidRecordSize`. It compares its
2272        // upper bound of the serialized size with `max.request.size` first,
2273        // and then with `buffer.memory`.
2274        let serialized_size = record_size_upper_bound(
2275            record.key.as_deref(),
2276            record.value.as_deref(),
2277            &record.headers,
2278        );
2279        if let Some(limit) = self.record_size_limit(serialized_size) {
2280            return Err(ProducerError::RecordTooLarge {
2281                record_size: serialized_size,
2282                limit,
2283            });
2284        }
2285        let record_size = approx_record_size(
2286            record.key.as_deref(),
2287            record.value.as_deref(),
2288            &record.headers,
2289        );
2290
2291        let timestamp = record.timestamp_ms.unwrap_or_else(current_millis);
2292        let mut memory = None;
2293        loop {
2294            // Kafka's `RecordAccumulator.append` peeks the sticky partition
2295            // before it takes the partition lock, and checks under the lock
2296            // that no other send switched it. A switch starts the loop again.
2297            // Memory is not bound to a partition, so it serves the new one.
2298            let sticky = fixed_partition
2299                .is_none()
2300                .then(|| self.peek_sticky(&record.topic, &partitions));
2301            let partition = fixed_partition
2302                .or_else(|| sticky.map(StickyPartition::partition))
2303                .unwrap_or_default();
2304            tracing::Span::current().record("partition", partition);
2305            let acc = Arc::clone(
2306                self.accumulators
2307                    .entry((record.topic.clone(), partition))
2308                    .or_insert_with(|| Arc::new(Mutex::new(Accumulator::new(self.batch_size))))
2309                    .value(),
2310            );
2311
2312            // A record that starts a new batch needs buffer memory. The wait
2313            // holds no lock, so the sender can complete batches and free
2314            // memory. Kafka's `RecordAccumulator.append` also allocates
2315            // outside the partition lock, and tries the append again after.
2316            //
2317            // The state check comes first, so a send that cannot append fails
2318            // at once. It releases the state lock before it takes the
2319            // accumulator lock: the append below takes the two locks in the
2320            // other order.
2321            if memory.is_none() {
2322                let state = match &self.transactional_id {
2323                    Some(_) => Some(*self.txn_state.lock().await),
2324                    None => None,
2325                };
2326                let expected_generation = self.append_transaction_generation(state)?;
2327                let needs_memory = {
2328                    let mut a = acc.lock().await;
2329                    if self.sticky_partition_changed(&record.topic, sticky, &a, &partitions) {
2330                        continue;
2331                    }
2332                    let needs_memory = a.needs_new_batch(record_size, expected_generation);
2333                    // Kafka closes a batch that the record does not fit, so
2334                    // the sender can send it and free its memory during the
2335                    // wait.
2336                    if needs_memory && a.current.as_ref().is_some_and(|batch| !batch.is_empty()) {
2337                        a.seal_current();
2338                        drop(a);
2339                        let _ = self.wake_tx.try_send(DrainIntent::Ready);
2340                    }
2341                    needs_memory
2342                };
2343                if needs_memory {
2344                    let size = self.batch_size.max(record_size);
2345                    memory = Some(self.buffer_pool.allocate(size, remaining_block).await?);
2346                }
2347            }
2348
2349            // Keep the state lock until the record is registered and appended.
2350            // `prepare_transaction` takes the same lock before it flushes, so
2351            // it cannot race past a send that has already joined this
2352            // transaction.
2353            let mut transaction_state = if self.transactional_id.is_some() {
2354                Some(self.txn_state.lock().await)
2355            } else {
2356                None
2357            };
2358            let transaction_generation =
2359                self.append_transaction_generation(transaction_state.as_deref().copied())?;
2360            if transaction_generation.is_some()
2361                && let Err(error) = self
2362                    .register_transaction_partition(&record.topic, partition)
2363                    .await
2364            {
2365                // The state lock is held here, so the state moves under it.
2366                if let (Some(state), Some(fatal)) =
2367                    (transaction_state.as_deref_mut(), self.txn_error.fatal())
2368                {
2369                    *state = fatal.state();
2370                }
2371                return Err(error);
2372            }
2373            let mut a = self
2374                .lock_accumulator_for_append(
2375                    &acc,
2376                    transaction_state.as_deref().copied(),
2377                    transaction_generation,
2378                )
2379                .await?;
2380            if self.sticky_partition_changed(&record.topic, sticky, &a, &partitions) {
2381                // Another send switched the sticky partition. Adding this
2382                // partition to the transaction has no effect on the outcome:
2383                // the transaction ends with no record on it.
2384                continue;
2385            }
2386            if memory.is_none() && a.needs_new_batch(record_size, transaction_generation) {
2387                // Another send filled the batch since the check above. Wait
2388                // for memory again, with no lock held. Adding the partition to
2389                // the transaction again has no effect.
2390                continue;
2391            }
2392            self.is_active()?;
2393            let AppendResult {
2394                receiver: rx,
2395                wakes_sender,
2396            } = a.append(
2397                record.key,
2398                record.value,
2399                record.headers,
2400                timestamp,
2401                transaction_generation,
2402                memory.take(),
2403            );
2404            drop(memory);
2405            if let Some(sticky) = sticky {
2406                // Kafka's `RecordAccumulator.updatePartitionInfoOnAppend`.
2407                self.update_sticky(
2408                    &record.topic,
2409                    sticky,
2410                    record_size,
2411                    &partitions,
2412                    a.all_batches_full(),
2413                );
2414            }
2415            drop(a);
2416            drop(acc);
2417            drop(transaction_state);
2418            wake_sender_after_append(&self.wake_tx, self.linger, wakes_sender);
2419            return Ok(DeliveryHandle::new(rx));
2420        }
2421    }
2422
2423    /// Tell if the append must pick its partition again. Kafka's
2424    /// `RecordAccumulator.partitionChanged`.
2425    ///
2426    /// Another send can have switched the sticky partition. When no batch of
2427    /// the partition is open, a switch that an open batch deferred happens
2428    /// now. The caller holds the lock of the accumulator `accumulator`.
2429    fn sticky_partition_changed(
2430        &self,
2431        topic: &str,
2432        sticky: Option<StickyPartition>,
2433        accumulator: &Accumulator,
2434        partitions: &TopicPartitions<'_>,
2435    ) -> bool {
2436        let Some(sticky) = sticky else {
2437            return false;
2438        };
2439        if self.partitioner.is_changed(topic, sticky) {
2440            return true;
2441        }
2442        if accumulator.all_batches_full() {
2443            self.update_sticky(topic, sticky, 0, partitions, true);
2444            return self.partitioner.is_changed(topic, sticky);
2445        }
2446        false
2447    }
2448
2449    /// The rack of the leader of `partition` of `topic`, or `None` when the
2450    /// leader is unknown or has no rack. `partitioner.rack.aware` uses it to
2451    /// keep the sticky partition of the built-in partitioner in
2452    /// [`Self::client_rack`].
2453    fn leader_rack(&self, topic: &str, partition: i32) -> Option<String> {
2454        let leader = *self.partition_leaders.get(&(topic.to_owned(), partition))?;
2455        if leader < 0 {
2456            return None;
2457        }
2458        let rack = self.broker_racks.get(&leader)?;
2459        rack.clone()
2460    }
2461
2462    /// [`BuiltInPartitioner::peek`], with `partitioner.rack.aware` when
2463    /// `client_rack` is set and the built-in partitioner's config asks for it.
2464    fn peek_sticky(&self, topic: &str, partitions: &TopicPartitions<'_>) -> StickyPartition {
2465        match self.client_rack.as_deref() {
2466            Some(client_rack) if self.partitioner.config().rack_aware => {
2467                let leader_rack = |partition: i32| self.leader_rack(topic, partition);
2468                let rack = RackPreference {
2469                    leader_rack: &leader_rack,
2470                    client_rack,
2471                };
2472                self.partitioner.peek_rack_aware(topic, partitions, rack)
2473            }
2474            _ => self.partitioner.peek(topic, partitions),
2475        }
2476    }
2477
2478    /// [`BuiltInPartitioner::update`], with the same rack preference as
2479    /// [`Self::peek_sticky`].
2480    fn update_sticky(
2481        &self,
2482        topic: &str,
2483        sticky: StickyPartition,
2484        appended: usize,
2485        partitions: &TopicPartitions<'_>,
2486        enable_switch: bool,
2487    ) {
2488        match self.client_rack.as_deref() {
2489            Some(client_rack) if self.partitioner.config().rack_aware => {
2490                let leader_rack = |partition: i32| self.leader_rack(topic, partition);
2491                let rack = RackPreference {
2492                    leader_rack: &leader_rack,
2493                    client_rack,
2494                };
2495                self.partitioner.update_rack_aware(
2496                    topic,
2497                    sticky,
2498                    appended,
2499                    partitions,
2500                    enable_switch,
2501                    rack,
2502                );
2503            }
2504            _ => self
2505                .partitioner
2506                .update(topic, sticky, appended, partitions, enable_switch),
2507        }
2508    }
2509
2510    /// The partitions of `topic` as Kafka's `Cluster` gives them to a custom
2511    /// [`Partitioner`]: every partition (`all_partitions`), and the subset
2512    /// with a known leader (`available_partitions`).
2513    fn partition_infos(&self, topic: &str, count: i32) -> (Vec<PartitionInfo>, Vec<PartitionInfo>) {
2514        let all: Vec<PartitionInfo> = (0..count)
2515            .map(|partition| {
2516                let leader_id = self
2517                    .partition_leaders
2518                    .get(&(topic.to_owned(), partition))
2519                    .map(|leader| *leader)
2520                    .filter(|leader| *leader >= 0);
2521                let leader_rack = leader_id.and_then(|id| self.leader_rack_of_broker(id));
2522                PartitionInfo {
2523                    partition,
2524                    leader_id,
2525                    leader_rack,
2526                }
2527            })
2528            .collect();
2529        let available = all
2530            .iter()
2531            .filter(|info| info.leader_id.is_some())
2532            .cloned()
2533            .collect();
2534        (available, all)
2535    }
2536
2537    /// The rack of broker `id`, or `None` when it is not known or the broker
2538    /// has none.
2539    fn leader_rack_of_broker(&self, id: i32) -> Option<String> {
2540        let rack = self.broker_racks.get(&id)?;
2541        rack.clone()
2542    }
2543
2544    /// The limit that a record of `serialized_size` bytes is larger than, in
2545    /// the order of Kafka's `KafkaProducer.ensureValidRecordSize`.
2546    fn record_size_limit(&self, serialized_size: usize) -> Option<RecordSizeLimit> {
2547        if serialized_size > self.max_request_size {
2548            Some(RecordSizeLimit::MaxRequestSize(self.max_request_size))
2549        } else if serialized_size > self.buffer_pool.total() {
2550            Some(RecordSizeLimit::BufferMemory)
2551        } else {
2552            None
2553        }
2554    }
2555
2556    /// Recheck recovery after waiting for the queue lock. A recovery sweep can
2557    /// visit this queue while an append waits, and then cache the new generation.
2558    /// Reject that append before it can insert an old-generation batch behind
2559    /// an occupied partition, where draining would otherwise defer its failure.
2560    async fn lock_accumulator_for_append<'a>(
2561        &self,
2562        accumulator: &'a Mutex<Accumulator>,
2563        state: Option<TxnState>,
2564        expected_generation: Option<u64>,
2565    ) -> Result<MutexGuard<'a, Accumulator>, ProducerError> {
2566        let queue = accumulator.lock().await;
2567        if self.append_transaction_generation(state)? != expected_generation {
2568            return Err(ProducerError::RecoveryRequired);
2569        }
2570        Ok(queue)
2571    }
2572
2573    /// The transaction generation that a send appends with in `state`.
2574    ///
2575    /// This is [`Self::transaction_generation`], after the check that Kafka's
2576    /// `TransactionManager.maybeAddPartition` makes with `maybeFailWithError`:
2577    /// a transactional producer in the fatal or the abortable error state
2578    /// fails the send with the stored error. Kafka makes this check after
2579    /// `waitOnMetadata`, so `send` calls this after its metadata wait.
2580    ///
2581    /// # Errors
2582    ///
2583    /// Returns the stored transaction error, or an error of
2584    /// [`Self::transaction_generation`].
2585    fn append_transaction_generation(
2586        &self,
2587        state: Option<TxnState>,
2588    ) -> Result<Option<u64>, ProducerError> {
2589        if state.is_some()
2590            && let Some(error) = self.transaction_error_state()
2591        {
2592            return Err(error);
2593        }
2594        self.transaction_generation(state)
2595    }
2596
2597    /// The transaction generation that a send takes in `state`: the recovery
2598    /// generation inside a transaction, and `None` when `state` is `None`
2599    /// (a non-transactional or idempotent-only producer).
2600    ///
2601    /// # Errors
2602    ///
2603    /// Returns an error when a transaction needs recovery, or when `state`
2604    /// names a transactional producer with no open transaction (`Kafka`'s
2605    /// `TransactionManager.maybeAddPartition` throws `IllegalStateException`
2606    /// for the same case).
2607    fn transaction_generation(
2608        &self,
2609        state: Option<TxnState>,
2610    ) -> Result<Option<u64>, ProducerError> {
2611        if self.transaction_recovery_required() {
2612            return Err(ProducerError::RecoveryRequired);
2613        }
2614        match state {
2615            Some(TxnState::InTransaction) => {
2616                Ok(Some(self.txn_recovery_generation.load(Ordering::Acquire)))
2617            }
2618            Some(TxnState::Preparing | TxnState::Prepared) => {
2619                Err(ProducerError::InvalidTransactionState(
2620                    "send is not allowed after prepare_transaction",
2621                ))
2622            }
2623            Some(_) => Err(ProducerError::InvalidTransactionState(
2624                "cannot send records while no transaction is in progress",
2625            )),
2626            None => Ok(None),
2627        }
2628    }
2629
2630    /// Return the partition count of `topic`. On a cache miss, or when
2631    /// `partition` is not below the cached count, wait for metadata for at
2632    /// most `max_block`. See [`MetadataWait::partition_count`].
2633    ///
2634    /// The wait uses [`Client::refresh_metadata_with`] rather than a bare
2635    /// `send(MetadataRequest)`. The request names the topics of the producer
2636    /// (see [`metadata_request`]). `refresh_metadata_with` also teaches the
2637    /// client's `BrokerPool` each broker's `(id → addr)` mapping, and that is
2638    /// what lets the sender route a Produce to the partition *leader* with
2639    /// `Client::broker(id)`. The wait records each partition's `leader_id` in
2640    /// `partition_leaders` for the sender to consult.
2641    #[tracing::instrument(
2642        level = "debug",
2643        skip_all,
2644        fields(topic = %topic, num_partitions = field::Empty),
2645    )]
2646    pub(crate) async fn partition_count(
2647        &self,
2648        topic: &str,
2649        partition: Option<i32>,
2650    ) -> Result<i32, ProducerError> {
2651        // Kafka's `KafkaProducer.waitOnMetadata` calls `ProducerMetadata.add`
2652        // for each send, so the periodic and error refreshes name the topic.
2653        self.client.metadata_topics().add(topic);
2654        let count = self
2655            .metadata_wait()
2656            .partition_count(topic, partition, |topics| {
2657                self.refresh_producer_metadata(topics)
2658            })
2659            .await?;
2660        tracing::Span::current().record("num_partitions", count);
2661        Ok(count)
2662    }
2663
2664    /// The metadata wait of this producer, limited by `max_block`.
2665    fn metadata_wait(&self) -> MetadataWait<'_> {
2666        MetadataWait {
2667            cache: &self.metadata_cache,
2668            partition_leaders: &self.partition_leaders,
2669            refresh: &self.metadata_refresh,
2670            max_block: self.max_block,
2671            retry_backoff: self.init_retry_backoff.to_std(),
2672            max_backoff: self.retry_backoff_max.to_std(),
2673        }
2674    }
2675
2676    /// Send the `Metadata` request of the producer for `topics`, and record
2677    /// the rack of each broker of the response.
2678    async fn refresh_producer_metadata(
2679        &self,
2680        topics: Vec<String>,
2681    ) -> Result<MetadataResponse, ClientError> {
2682        let response = self
2683            .client
2684            .refresh_metadata_with(metadata_request(topics))
2685            .await?;
2686        // Kafka's `Cluster` keeps each broker's rack alongside its partition
2687        // leaders. `partitioner.rack.aware` reads it through `leader_rack`.
2688        for broker in &response.brokers {
2689            self.broker_racks
2690                .insert(broker.node_id, broker.rack.clone());
2691        }
2692        Ok(response)
2693    }
2694
2695    #[tracing::instrument(
2696        level = "info",
2697        skip_all,
2698        fields(client_id = %self.client_id, transactional_id = self.transactional_id.as_deref()),
2699        err(level = "info"),
2700    )]
2701    /// # Errors
2702    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
2703    pub async fn close(mut self) -> Result<(), ProducerError> {
2704        self.flush().await?;
2705        // The terminating telemetry push (KIP-714).
2706        self.client.close_telemetry().await;
2707        self.state.store(STATE_CLOSED, Ordering::Release);
2708        self.sender_shutdown.cancel();
2709        if let Some(h) = self.sender_handle.take() {
2710            let _ = h.await;
2711        }
2712        Ok(())
2713    }
2714
2715    /// The client instance id that the broker assigned for metrics push
2716    /// (KIP-714), as Kafka's `KafkaProducer.clientInstanceId` returns it.
2717    ///
2718    /// The call waits up to `timeout` for the first
2719    /// `GetTelemetrySubscriptions` response. It returns `Ok(None)` when none
2720    /// has come by then, as Kafka returns `null`. A zero `timeout` does not
2721    /// wait.
2722    ///
2723    /// # Errors
2724    /// Returns [`ClientError::TelemetryDisabled`] when `enable_metrics_push`
2725    /// is `false`, and [`ClientError::InvalidArgument`] for a negative
2726    /// `timeout`, as Kafka throws an `IllegalStateException` and an
2727    /// `IllegalArgumentException`.
2728    ///
2729    /// [`ClientError::TelemetryDisabled`]: ClientError::TelemetryDisabled
2730    /// [`ClientError::InvalidArgument`]: ClientError::InvalidArgument
2731    pub async fn client_instance_id(
2732        &self,
2733        timeout: Time,
2734    ) -> Result<Option<uuid::Uuid>, ProducerError> {
2735        Ok(self.client.client_instance_id(timeout).await?)
2736    }
2737
2738    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
2739    /// # Errors
2740    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
2741    pub async fn flush(&self) -> Result<(), ProducerError> {
2742        self.is_active()?;
2743        let deadline = Instant::now()
2744            .checked_add(self.flush_timeout.duration())
2745            .ok_or(ProducerError::FlushTimeout)?;
2746
2747        time::timeout_at(deadline, async {
2748            let _ = self.wake_tx.send(DrainIntent::Force).await;
2749            loop {
2750                let notified = self.flush_notify.notified();
2751                tokio::pin!(notified);
2752                notified.as_mut().enable();
2753                if self.all_empty().await && self.in_flight.load(Ordering::Acquire) == 0 {
2754                    return;
2755                }
2756                notified.await;
2757            }
2758        })
2759        .await
2760        .map_err(|_| ProducerError::FlushTimeout)
2761    }
2762
2763    /// Fail every batch still queued in the accumulators — not yet drained
2764    /// onto the wire — with [`ProducerError::TransactionAborted`], sending no
2765    /// `Produce` for it. Then wait, up to `flush_timeout`, only for batches
2766    /// already in flight (already sent to the wire, awaiting ack) to
2767    /// complete normally.
2768    ///
2769    /// `end_transaction` calls this instead of [`Self::flush`] when aborting,
2770    /// matching Kafka's `Sender.maybeSendAndPollTransactionalRequest`:
2771    /// `accumulator.abortUndrainedBatches(new TransactionAbortedException())`.
2772    /// Kafka's `KafkaProducer.abortTransaction` likewise does not flush.
2773    async fn abort_undrained_batches(&self) -> Result<(), ProducerError> {
2774        self.is_active()?;
2775        for entry in self.accumulators.iter() {
2776            let mut a = entry.value().lock().await;
2777            a.abort_undrained_batches();
2778        }
2779
2780        let deadline = Instant::now()
2781            .checked_add(self.flush_timeout.duration())
2782            .ok_or(ProducerError::FlushTimeout)?;
2783        time::timeout_at(deadline, async {
2784            loop {
2785                let notified = self.flush_notify.notified();
2786                tokio::pin!(notified);
2787                notified.as_mut().enable();
2788                if self.in_flight.load(Ordering::Acquire) == 0 {
2789                    return;
2790                }
2791                notified.await;
2792            }
2793        })
2794        .await
2795        .map_err(|_| ProducerError::FlushTimeout)
2796    }
2797
2798    async fn all_empty(&self) -> bool {
2799        for entry in self.accumulators.iter() {
2800            let a = entry.value().lock().await;
2801            if a.current.as_ref().is_some_and(|b| !b.is_empty()) {
2802                return false;
2803            }
2804            if !a.ready.is_empty() {
2805                return false;
2806            }
2807        }
2808        true
2809    }
2810}
2811
2812impl fmt::Debug for Producer {
2813    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2814        f.debug_struct("Producer")
2815            .field("producer_id", &self.producer_id())
2816            .field("producer_epoch", &self.producer_epoch())
2817            .field("transactional_id", &self.transactional_id)
2818            .field("compression", &self.compression)
2819            .field(
2820                "compression_level",
2821                &self.compression_levels.level(self.compression),
2822            )
2823            .finish_non_exhaustive()
2824    }
2825}
2826
2827fn current_millis() -> i64 {
2828    i64::try_from(
2829        SystemTime::now()
2830            .duration_since(UNIX_EPOCH)
2831            .map_or(0, |d| d.as_millis()),
2832    )
2833    .unwrap_or(0)
2834}
2835
2836/// Group `((topic, partition), offset)` pairs by topic into the nested
2837/// structure required by [`TxnOffsetCommitRequest`], in the order in which
2838/// each topic first appears.
2839///
2840/// Each topic carries its name and the id that `topic_ids` gives, or
2841/// [`Uuid::ZERO`] for a topic without one, as Kafka's
2842/// `TransactionManager.txnOffsetCommitHandler` sets both. The negotiated
2843/// version decides which of the two goes on the wire.
2844fn build_topics_payload(
2845    offsets: &[((String, i32), i64)],
2846    topic_ids: &HashMap<String, Uuid>,
2847) -> Vec<TxnOffsetCommitRequestTopic> {
2848    let mut topics: Vec<TxnOffsetCommitRequestTopic> = Vec::new();
2849    for ((topic, partition), offset) in offsets {
2850        let partition = TxnOffsetCommitRequestPartition {
2851            partition_index: *partition,
2852            committed_offset: *offset,
2853            ..Default::default()
2854        };
2855        match topics.iter_mut().find(|entry| entry.name == *topic) {
2856            Some(entry) => entry.partitions.push(partition),
2857            None => topics.push(TxnOffsetCommitRequestTopic {
2858                name: topic.clone(),
2859                topic_id: topic_ids.get(topic).copied().unwrap_or(Uuid::ZERO),
2860                partitions: vec![partition],
2861                ..Default::default()
2862            }),
2863        }
2864    }
2865    topics
2866}
2867
2868#[cfg(test)]
2869mod tests {
2870    use std::{
2871        collections::BTreeSet,
2872        io,
2873        sync::{
2874            Arc, Mutex,
2875            atomic::{AtomicBool, AtomicU16, AtomicUsize, Ordering},
2876        },
2877        task::Poll,
2878        time::{Duration, Instant},
2879    };
2880
2881    use bytes::{Bytes, BytesMut};
2882    use krabka_client_core::MockBroker;
2883    use krabka_protocol::{
2884        Decode, Encode,
2885        owned::{
2886            api_versions_request,
2887            api_versions_response::{ApiVersion, ApiVersionsResponse},
2888            find_coordinator_request::{self, FindCoordinatorRequest},
2889            find_coordinator_response::{self, Coordinator, FindCoordinatorResponse},
2890            metadata_request::{self, MetadataRequest},
2891            metadata_response::{
2892                self, MetadataResponse, MetadataResponseBroker, MetadataResponsePartition,
2893                MetadataResponseTopic,
2894            },
2895            produce_request::{self, ProduceRequest},
2896            produce_response::{PartitionProduceResponse, ProduceResponse, TopicProduceResponse},
2897        },
2898        primitives::uuid::Uuid,
2899    };
2900    use tokio::{
2901        sync::{Mutex as AsyncMutex, mpsc},
2902        time,
2903    };
2904
2905    use super::{
2906        DrainIntent, Producer, TxnOffsetCommitCap, TxnOffsetCommitRequest,
2907        TxnOffsetCommitRequestPartition, TxnOffsetCommitRequestTopic, TxnOffsetCommitResponse,
2908        build_topics_payload, minted_identity, request_may_have_reached_the_broker,
2909        txn_offset_commit_error_code, wake_sender_after_append,
2910    };
2911    use crate::{
2912        ProducerRecord,
2913        accumulator::{Accumulator, AppendResult},
2914        builder::DEFAULT_PRODUCER_MAX_BLOCK,
2915        compression::Compression,
2916        error::ProducerError,
2917        partitioner::{PartitionInfo, Partitioner, partition_for_key},
2918        transactional::TxnState,
2919    };
2920
2921    /// A custom partitioner that always picks one partition, and records the
2922    /// partitions it was shown.
2923    struct FixedPartitioner {
2924        pick: i32,
2925        shown: Mutex<Vec<(Vec<PartitionInfo>, Vec<PartitionInfo>)>>,
2926    }
2927
2928    impl Partitioner for FixedPartitioner {
2929        fn partition(
2930            &self,
2931            _topic: &str,
2932            _key: Option<&[u8]>,
2933            _value: Option<&[u8]>,
2934            available_partitions: &[PartitionInfo],
2935            all_partitions: &[PartitionInfo],
2936        ) -> i32 {
2937            self.shown
2938                .lock()
2939                .unwrap()
2940                .push((available_partitions.to_vec(), all_partitions.to_vec()));
2941            self.pick
2942        }
2943    }
2944
2945    /// Kafka's `KafkaProducer.partition`: a custom partitioner picks the
2946    /// partition of a record that names none, keyed or not. A negative pick
2947    /// fails the send with Kafka's message, and no Produce goes out.
2948    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2949    async fn a_custom_partitioner_picks_the_partition_and_a_negative_pick_fails_the_send() {
2950        let negative = "The partitioner generated an invalid partition number: -1. Partition \
2951                        number should always be non-negative.";
2952        let leader = |partition| PartitionInfo {
2953            partition,
2954            leader_id: Some(1),
2955            leader_rack: None,
2956        };
2957        let shown_partitions = (0..4).map(leader).collect::<Vec<_>>();
2958        let cases = [
2959            ("keyless, pick 2", None, 2, Ok(2), vec![2]),
2960            (
2961                "keyed, pick 2",
2962                Some(b"kafka".as_slice()),
2963                2,
2964                Ok(2),
2965                vec![2],
2966            ),
2967            ("pick -1", None, -1, Err(negative.to_owned()), vec![]),
2968        ];
2969        let mut actual = Vec::new();
2970        let mut expected = Vec::new();
2971        for (name, key, pick, delivered, produced_partitions) in cases {
2972            let partitioner = Arc::new(FixedPartitioner {
2973                pick,
2974                shown: Mutex::new(Vec::new()),
2975            });
2976            let outcome = send_with_partitioner(
2977                vec![MetadataAnswer::Topic {
2978                    error_code: 0,
2979                    partitions: 4,
2980                }],
2981                Duration::from_secs(5),
2982                ProducerRecord {
2983                    topic: METADATA_TOPIC.into(),
2984                    key: key.map(Bytes::from_static),
2985                    value: Some(Bytes::from_static(b"v")),
2986                    ..Default::default()
2987                },
2988                Some(Arc::clone(&partitioner) as Arc<dyn Partitioner>),
2989            )
2990            .await;
2991            actual.push((
2992                name,
2993                outcome.delivered,
2994                outcome.produced_partitions,
2995                partitioner.shown.lock().unwrap().clone(),
2996            ));
2997            expected.push((
2998                name,
2999                delivered,
3000                produced_partitions,
3001                vec![(shown_partitions.clone(), shown_partitions.clone())],
3002            ));
3003        }
3004        assert2::assert!(actual == expected);
3005    }
3006
3007    /// Both halves of the pair must be non-negative. Kafka writes `-1` in
3008    /// either half for "no producer", so a check of one half alone would
3009    /// report a half-written pair as a minted identity.
3010    #[test]
3011    fn a_minted_identity_needs_both_halves_of_the_pair() {
3012        assert2::check!(minted_identity((4242, 7)) == Some((4242, 7)));
3013        assert2::check!(minted_identity((0, 0)) == Some((0, 0)));
3014        assert2::check!(minted_identity((-1, -1)).is_none());
3015        // Either half alone being negative is not a minted identity.
3016        assert2::check!(minted_identity((4242, -1)).is_none());
3017        assert2::check!(minted_identity((-1, 7)).is_none());
3018    }
3019
3020    const CLIENT_ID: &str = "producer-test";
3021
3022    #[test]
3023    fn only_new_deadlines_and_rollovers_wake_nonzero_linger() {
3024        let (wake_tx, mut wake_rx) = mpsc::channel(4);
3025
3026        let mut coalesced = Accumulator::new(1024);
3027        let AppendResult { wakes_sender, .. } =
3028            coalesced.try_append(None, Some(Bytes::from_static(b"a")), vec![], 0, None);
3029        wake_sender_after_append(&wake_tx, krabka_units::millis(10), wakes_sender);
3030        assert_eq!(wake_rx.try_recv(), Ok(DrainIntent::Ready));
3031        let AppendResult { wakes_sender, .. } =
3032            coalesced.try_append(None, Some(Bytes::from_static(b"b")), vec![], 0, None);
3033        drop(coalesced);
3034        wake_sender_after_append(&wake_tx, krabka_units::millis(10), wakes_sender);
3035        assert_eq!(wake_rx.try_recv(), Err(mpsc::error::TryRecvError::Empty));
3036
3037        let mut rollover = Accumulator::new(20);
3038        let _ = rollover.try_append(None, Some(Bytes::from_static(b"a")), vec![], 0, None);
3039        let AppendResult { wakes_sender, .. } =
3040            rollover.try_append(None, Some(Bytes::from_static(b"b")), vec![], 0, None);
3041        drop(rollover);
3042        wake_sender_after_append(&wake_tx, krabka_units::millis(10), wakes_sender);
3043        assert_eq!(wake_rx.try_recv(), Ok(DrainIntent::Ready));
3044
3045        let mut immediate = Accumulator::new(1024);
3046        let _ = immediate.try_append(None, Some(Bytes::from_static(b"a")), vec![], 0, None);
3047        drop(immediate);
3048        wake_sender_after_append(&wake_tx, krabka_units::secs(0), false);
3049        assert_eq!(wake_rx.try_recv(), Ok(DrainIntent::Force));
3050    }
3051
3052    fn encode_v0(resp: &impl Encode) -> Vec<u8> {
3053        let mut buf = BytesMut::new();
3054        resp.encode(&mut buf, 0).unwrap();
3055        buf.to_vec()
3056    }
3057
3058    fn encode_find_coordinator_response(version: i16) -> Vec<u8> {
3059        let resp = if version >= 4 {
3060            FindCoordinatorResponse {
3061                coordinators: vec![Coordinator {
3062                    key: "group-a".into(),
3063                    node_id: 1,
3064                    host: "127.0.0.1".into(),
3065                    port: 19092,
3066                    ..Default::default()
3067                }],
3068                ..Default::default()
3069            }
3070        } else {
3071            FindCoordinatorResponse {
3072                node_id: 1,
3073                host: "127.0.0.1".into(),
3074                port: 19092,
3075                ..Default::default()
3076            }
3077        };
3078        let mut buf = BytesMut::new();
3079        if version >= find_coordinator_response::FLEXIBLE_MIN {
3080            buf.extend_from_slice(&[0]);
3081        }
3082        resp.encode(&mut buf, version).unwrap();
3083        buf.to_vec()
3084    }
3085
3086    fn api_versions_response(find_coordinator_version: i16) -> Vec<u8> {
3087        encode_v0(&ApiVersionsResponse {
3088            api_keys: vec![
3089                ApiVersion {
3090                    api_key: api_versions_request::API_KEY,
3091                    min_version: 0,
3092                    max_version: 0,
3093                    ..Default::default()
3094                },
3095                ApiVersion {
3096                    api_key: find_coordinator_request::API_KEY,
3097                    min_version: find_coordinator_version,
3098                    max_version: find_coordinator_version,
3099                    ..Default::default()
3100                },
3101            ],
3102            ..Default::default()
3103        })
3104    }
3105
3106    fn decode_request_body(body: &[u8], version: i16) -> FindCoordinatorRequest {
3107        let header_len = 2 + CLIENT_ID.len() + usize::from(version >= 3);
3108        let mut request_body = &body[header_len..];
3109        FindCoordinatorRequest::decode(&mut request_body, version).unwrap()
3110    }
3111
3112    async fn producer_with_find_coordinator_version(
3113        version: i16,
3114        seen: Arc<Mutex<Vec<FindCoordinatorRequest>>>,
3115    ) -> (MockBroker, Producer) {
3116        let seen_by_handler = Arc::clone(&seen);
3117        let find_coordinator_version = version;
3118        let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3119            if api_key == api_versions_request::API_KEY {
3120                return Some(api_versions_response(find_coordinator_version));
3121            }
3122            if api_key == find_coordinator_request::API_KEY {
3123                seen_by_handler
3124                    .lock()
3125                    .unwrap()
3126                    .push(decode_request_body(body, version));
3127                return Some(encode_find_coordinator_response(version));
3128            }
3129            None
3130        })
3131        .await
3132        .unwrap();
3133        let producer = Producer::builder()
3134            .bootstrap(mock.addr.to_string())
3135            .client_id(CLIENT_ID)
3136            .enable_idempotence(false)
3137            .build()
3138            .await
3139            .expect("producer connects to mock broker");
3140        (mock, producer)
3141    }
3142
3143    async fn producer_with_flush_timeout(flush_timeout: Duration) -> (MockBroker, Producer) {
3144        let mock = MockBroker::start(|api_key, _version, _corr_id, _body| {
3145            (api_key == api_versions_request::API_KEY).then(|| api_versions_response(4))
3146        })
3147        .await
3148        .unwrap();
3149        let producer = Producer::builder()
3150            .bootstrap(mock.addr.to_string())
3151            .client_id(CLIENT_ID)
3152            .enable_idempotence(false)
3153            .flush_timeout(flush_timeout)
3154            .build()
3155            .await
3156            .expect("producer connects to mock broker");
3157        (mock, producer)
3158    }
3159
3160    #[derive(Clone, Copy)]
3161    enum LookupKind {
3162        Group,
3163        Transaction,
3164    }
3165
3166    async fn find_coordinator(producer: &Producer, kind: LookupKind, key: &str) -> String {
3167        let deadline = time::Instant::now() + Duration::from_secs(5);
3168        match kind {
3169            LookupKind::Group => producer.find_group_coordinator(key, deadline).await,
3170            LookupKind::Transaction => producer.find_txn_coordinator(key, deadline).await,
3171        }
3172        .expect("coordinator is returned")
3173    }
3174
3175    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3176    async fn find_coordinator_sends_expected_request_for_legacy_and_batched_versions() {
3177        for (_name, version, kind, key, expected_request) in [
3178            (
3179                "legacy group",
3180                3,
3181                LookupKind::Group,
3182                "group-a",
3183                FindCoordinatorRequest {
3184                    key: "group-a".into(),
3185                    ..Default::default()
3186                },
3187            ),
3188            (
3189                "batched group",
3190                4,
3191                LookupKind::Group,
3192                "group-a",
3193                FindCoordinatorRequest {
3194                    coordinator_keys: vec!["group-a".into()],
3195                    ..Default::default()
3196                },
3197            ),
3198            (
3199                "legacy transaction",
3200                3,
3201                LookupKind::Transaction,
3202                "txn-a",
3203                FindCoordinatorRequest {
3204                    key: "txn-a".into(),
3205                    key_type: 1,
3206                    ..Default::default()
3207                },
3208            ),
3209            (
3210                "batched transaction",
3211                4,
3212                LookupKind::Transaction,
3213                "txn-a",
3214                FindCoordinatorRequest {
3215                    key_type: 1,
3216                    coordinator_keys: vec!["txn-a".into()],
3217                    ..Default::default()
3218                },
3219            ),
3220        ] {
3221            let seen = Arc::new(Mutex::new(Vec::new()));
3222            let (mock, producer) =
3223                producer_with_find_coordinator_version(version, Arc::clone(&seen)).await;
3224
3225            let addr = find_coordinator(&producer, kind, key).await;
3226            assert2::assert!(addr == "127.0.0.1:19092");
3227            assert2::assert!(*seen.lock().unwrap() == vec![expected_request]);
3228            mock.stop();
3229        }
3230    }
3231
3232    #[tokio::test(start_paused = true)]
3233    async fn flush_times_out_at_the_configured_deadline() {
3234        let (mock, producer) = producer_with_flush_timeout(Duration::from_millis(7)).await;
3235        producer.in_flight.store(1, Ordering::Release);
3236
3237        let flush = producer.flush();
3238        tokio::pin!(flush);
3239        assert!(futures::poll!(flush.as_mut()).is_pending());
3240
3241        time::advance(Duration::from_millis(6)).await;
3242        assert!(futures::poll!(flush.as_mut()).is_pending());
3243
3244        time::advance(Duration::from_millis(1)).await;
3245        assert!(matches!(
3246            futures::poll!(flush.as_mut()),
3247            Poll::Ready(Err(ProducerError::FlushTimeout))
3248        ));
3249        mock.stop();
3250    }
3251
3252    #[tokio::test(start_paused = true)]
3253    async fn flush_timeout_bounds_a_blocked_force_wake() {
3254        let (mock, mut producer) = producer_with_flush_timeout(Duration::from_millis(7)).await;
3255        let (wake_tx, _wake_rx) = mpsc::channel(16);
3256        producer.wake_tx = wake_tx;
3257        for _ in 0..16 {
3258            producer
3259                .wake_tx
3260                .try_send(DrainIntent::Force)
3261                .expect("wake channel has capacity");
3262        }
3263
3264        let flush = producer.flush();
3265        tokio::pin!(flush);
3266        assert!(futures::poll!(flush.as_mut()).is_pending());
3267
3268        time::advance(Duration::from_millis(6)).await;
3269        assert!(futures::poll!(flush.as_mut()).is_pending());
3270
3271        time::advance(Duration::from_millis(1)).await;
3272        assert!(matches!(
3273            futures::poll!(flush.as_mut()),
3274            Poll::Ready(Err(ProducerError::FlushTimeout))
3275        ));
3276        mock.stop();
3277    }
3278
3279    #[tokio::test]
3280    async fn flush_does_not_miss_notification_during_state_check() {
3281        let (mock, producer) = producer_with_flush_timeout(Duration::from_millis(20)).await;
3282        let accumulator = Arc::new(AsyncMutex::new(Accumulator::new(1024)));
3283        producer
3284            .accumulators
3285            .insert(("held".to_owned(), 0), Arc::clone(&accumulator));
3286        let mut guard = accumulator.lock().await;
3287
3288        let flush = producer.flush();
3289        tokio::pin!(flush);
3290        assert!(futures::poll!(flush.as_mut()).is_pending());
3291
3292        guard.current = None;
3293        guard.ready.clear();
3294        producer.flush_notify.notify_waiters();
3295        drop(guard);
3296        drop(accumulator);
3297
3298        time::timeout(Duration::from_millis(20), flush)
3299            .await
3300            .expect("flush must not wait for another notification")
3301            .expect("empty producer flushes");
3302        mock.stop();
3303    }
3304
3305    #[tokio::test]
3306    async fn append_waiting_for_a_swept_queue_rejects_its_old_generation() {
3307        let (mock, producer) = producer_with_flush_timeout(Duration::from_millis(20)).await;
3308        let accumulator = AsyncMutex::new(Accumulator::new(1024));
3309        let transaction = Some(TxnState::InTransaction);
3310        {
3311            let queue = accumulator.lock().await;
3312            let expected = producer.append_transaction_generation(transaction).unwrap();
3313            let append = producer.lock_accumulator_for_append(&accumulator, transaction, expected);
3314            tokio::pin!(append);
3315            assert2::assert!(futures::poll!(append.as_mut()).is_pending());
3316
3317            // Recovery can sweep this locked queue before the append acquires
3318            // it. Completing reinitialization clears the required flag, but
3319            // must not allow the append captured in the previous generation
3320            // through.
3321            producer.require_transaction_recovery();
3322            producer
3323                .txn_recovery_required
3324                .store(false, Ordering::Release);
3325            drop(queue);
3326            assert2::assert!(matches!(append.await, Err(ProducerError::RecoveryRequired)));
3327        }
3328        assert2::assert!(accumulator.lock().await.current.is_none());
3329        let current = producer.append_transaction_generation(transaction).unwrap();
3330        let fresh = producer
3331            .lock_accumulator_for_append(&accumulator, transaction, current)
3332            .await
3333            .expect("a new-generation append can acquire the swept queue");
3334        assert2::assert!(fresh.current.is_none());
3335        drop(fresh);
3336        drop(accumulator);
3337        mock.stop();
3338    }
3339
3340    /// The version cap and the wire form of one `TxnOffsetCommit` request.
3341    #[derive(Debug, PartialEq, Eq)]
3342    struct TxnOffsetCommitShape {
3343        cap: TxnOffsetCommitCap,
3344        max_version: i16,
3345        /// The topics as the broker decodes them at `max_version`.
3346        wire_topics: Vec<TxnOffsetCommitRequestTopic>,
3347    }
3348
3349    /// Kafka's `TransactionManager.txnOffsetCommitHandler` sends v6, which
3350    /// names each topic by id (KIP-1319), only when the metadata holds an id
3351    /// for every topic and the producer follows transaction version 2. One
3352    /// topic without an id caps the request at v5, which names each topic,
3353    /// and transaction version 1 caps it at v4. The request groups the
3354    /// partitions of a topic in the order in which the topic first appears.
3355    #[test]
3356    fn txn_offset_commit_names_topics_by_id_only_at_v6() {
3357        let orders = Uuid([7; 16]);
3358        let payments = Uuid([8; 16]);
3359        let offsets = [
3360            (("orders".to_owned(), 0), 10),
3361            (("payments".to_owned(), 1), 20),
3362            (("orders".to_owned(), 2), 30),
3363        ];
3364        let partition = |partition_index, committed_offset| TxnOffsetCommitRequestPartition {
3365            partition_index,
3366            committed_offset,
3367            ..Default::default()
3368        };
3369        let topic = |name: &str, topic_id, partitions| TxnOffsetCommitRequestTopic {
3370            name: name.to_owned(),
3371            topic_id,
3372            partitions,
3373            ..Default::default()
3374        };
3375        let both_ids =
3376            maplit::hashmap! {"orders".to_owned() => orders, "payments".to_owned() => payments};
3377        let one_id = maplit::hashmap! {"orders".to_owned() => orders};
3378        for (name, topic_ids, transaction_v2, expected) in [
3379            (
3380                "every topic has an id",
3381                &both_ids,
3382                true,
3383                TxnOffsetCommitShape {
3384                    cap: TxnOffsetCommitCap::Latest,
3385                    max_version: 6,
3386                    wire_topics: vec![
3387                        topic("", orders, vec![partition(0, 10), partition(2, 30)]),
3388                        topic("", payments, vec![partition(1, 20)]),
3389                    ],
3390                },
3391            ),
3392            (
3393                "one topic has no id",
3394                &one_id,
3395                true,
3396                TxnOffsetCommitShape {
3397                    cap: TxnOffsetCommitCap::TopicNames,
3398                    max_version: 5,
3399                    wire_topics: vec![
3400                        topic(
3401                            "orders",
3402                            Uuid::ZERO,
3403                            vec![partition(0, 10), partition(2, 30)],
3404                        ),
3405                        topic("payments", Uuid::ZERO, vec![partition(1, 20)]),
3406                    ],
3407                },
3408            ),
3409            (
3410                "transaction version 1",
3411                &both_ids,
3412                false,
3413                TxnOffsetCommitShape {
3414                    cap: TxnOffsetCommitCap::TransactionV1,
3415                    max_version: 4,
3416                    wire_topics: vec![
3417                        topic(
3418                            "orders",
3419                            Uuid::ZERO,
3420                            vec![partition(0, 10), partition(2, 30)],
3421                        ),
3422                        topic("payments", Uuid::ZERO, vec![partition(1, 20)]),
3423                    ],
3424                },
3425            ),
3426        ] {
3427            let request = TxnOffsetCommitRequest {
3428                transactional_id: "txn".into(),
3429                group_id: "group".into(),
3430                topics: build_topics_payload(&offsets, topic_ids),
3431                ..Default::default()
3432            };
3433            let cap = TxnOffsetCommitCap::of(&request.topics, transaction_v2);
3434            let max_version = cap.max_version();
3435            let mut buf = BytesMut::new();
3436            request.encode(&mut buf, max_version).expect("encode");
3437            let decoded =
3438                TxnOffsetCommitRequest::decode(&mut buf.freeze(), max_version).expect("decode");
3439            let actual = TxnOffsetCommitShape {
3440                cap,
3441                max_version,
3442                wire_topics: decoded.topics,
3443            };
3444            assert2::assert!(actual == expected, "{name}");
3445        }
3446    }
3447
3448    /// A `TxnOffsetCommit` answer of several rows gives one code. A code that
3449    /// no rule retries wins. Among retriable rows, a row that asks for the
3450    /// coordinator again wins, because Kafka's `TxnOffsetCommitHandler` looks
3451    /// the coordinator up for the whole request at such a row.
3452    #[test]
3453    fn txn_offset_commit_reads_the_code_that_decides_the_answer() {
3454        use krabka_protocol::owned::txn_offset_commit_response::{
3455            TxnOffsetCommitResponsePartition, TxnOffsetCommitResponseTopic,
3456        };
3457        let response = |codes: &[i16]| TxnOffsetCommitResponse {
3458            topics: vec![TxnOffsetCommitResponseTopic {
3459                name: "topic".into(),
3460                partitions: codes
3461                    .iter()
3462                    .enumerate()
3463                    .map(|(index, error_code)| TxnOffsetCommitResponsePartition {
3464                        partition_index: i32::try_from(index).expect("partition index"),
3465                        error_code: *error_code,
3466                        ..Default::default()
3467                    })
3468                    .collect(),
3469                ..Default::default()
3470            }],
3471            ..Default::default()
3472        };
3473        for (name, codes, expected) in [
3474            ("every row committed", &[0, 0][..], 0),
3475            ("one retriable row", &[0, 14][..], 14),
3476            ("the coordinator moved", &[14, 16][..], 16),
3477            ("a timed out row asks for the coordinator", &[3, 7][..], 7),
3478            ("a code that no rule retries wins", &[16, 48][..], 48),
3479            ("an abortable code wins", &[14, 22][..], 22),
3480            ("the first row that resends stands", &[3, 14][..], 3),
3481        ] {
3482            assert2::assert!(
3483                txn_offset_commit_error_code(&response(codes)) == expected,
3484                "{name}"
3485            );
3486        }
3487    }
3488
3489    /// One answer of the scripted broker to a `Metadata` request.
3490    #[derive(Clone, Copy)]
3491    enum MetadataAnswer {
3492        /// The topic with this error code and this number of partitions.
3493        Topic { error_code: i16, partitions: i32 },
3494        /// No answer, so the request times out.
3495        Silent,
3496    }
3497
3498    /// What one send gave: the delivered partition or the error text, the
3499    /// partitions of every Produce that the broker received, and the distinct
3500    /// `Metadata` requests.
3501    #[derive(Debug, PartialEq, Eq)]
3502    struct SendOutcome {
3503        delivered: Result<i32, String>,
3504        produced_partitions: Vec<i32>,
3505        metadata_requests: BTreeSet<RequestedTopics>,
3506    }
3507
3508    /// The topics that a `Metadata` request names (`None` for all topics),
3509    /// and its `allow_auto_topic_creation`.
3510    type RequestedTopics = (Option<Vec<String>>, bool);
3511
3512    fn requested_topics(body: &[u8], version: i16) -> MetadataRequest {
3513        let header_len =
3514            2 + CLIENT_ID.len() + usize::from(version >= metadata_request::FLEXIBLE_MIN);
3515        let mut request_body = &body[header_len..];
3516        MetadataRequest::decode(&mut request_body, version).expect("decode Metadata")
3517    }
3518
3519    const METADATA_TOPIC: &str = "orders";
3520
3521    /// Encode `answer`. A broker lists a topic with an error only when the
3522    /// request names it: a request for all topics gives the topics that exist
3523    /// and that the client may describe.
3524    fn metadata_answer(
3525        version: i16,
3526        port: u16,
3527        answer: MetadataAnswer,
3528        named: bool,
3529    ) -> Option<Vec<u8>> {
3530        let MetadataAnswer::Topic {
3531            error_code,
3532            partitions,
3533        } = answer
3534        else {
3535            return None;
3536        };
3537        let listed = named || error_code == 0;
3538        let response = MetadataResponse {
3539            brokers: vec![MetadataResponseBroker {
3540                node_id: 1,
3541                host: "127.0.0.1".into(),
3542                port: i32::from(port),
3543                ..Default::default()
3544            }],
3545            topics: listed
3546                .then(|| MetadataResponseTopic {
3547                    error_code,
3548                    name: Some(METADATA_TOPIC.into()),
3549                    partitions: (0..partitions)
3550                        .map(|partition_index| MetadataResponsePartition {
3551                            partition_index,
3552                            leader_id: 1,
3553                            ..Default::default()
3554                        })
3555                        .collect(),
3556                    ..Default::default()
3557                })
3558                .into_iter()
3559                .collect(),
3560            ..Default::default()
3561        };
3562        let mut buf = BytesMut::new();
3563        if version >= metadata_response::FLEXIBLE_MIN {
3564            buf.extend_from_slice(&[0]);
3565        }
3566        response.encode(&mut buf, version).expect("encode Metadata");
3567        Some(buf.to_vec())
3568    }
3569
3570    fn produce_answer(request: &ProduceRequest) -> Vec<u8> {
3571        let response = ProduceResponse {
3572            responses: request
3573                .topic_data
3574                .iter()
3575                .map(|topic| TopicProduceResponse {
3576                    name: topic.name.clone(),
3577                    partition_responses: topic
3578                        .partition_data
3579                        .iter()
3580                        .map(|partition| PartitionProduceResponse {
3581                            index: partition.index,
3582                            base_offset: 0,
3583                            log_start_offset: -1,
3584                            ..Default::default()
3585                        })
3586                        .collect(),
3587                    ..Default::default()
3588                })
3589                .collect(),
3590            ..Default::default()
3591        };
3592        let mut buf = BytesMut::new();
3593        response
3594            .encode(&mut buf, PRODUCE_VERSION)
3595            .expect("encode Produce");
3596        buf.to_vec()
3597    }
3598
3599    const PRODUCE_VERSION: i16 = 3;
3600
3601    /// Start a broker that answers `Metadata` from `answers` in order, and
3602    /// repeats the last answer. It answers every Produce with success.
3603    async fn send_against_scripted_metadata(
3604        answers: Vec<MetadataAnswer>,
3605        max_block: Duration,
3606        record: ProducerRecord,
3607    ) -> SendOutcome {
3608        send_with_partitioner(answers, max_block, record, None).await
3609    }
3610
3611    /// [`send_against_scripted_metadata`], with a custom partitioner.
3612    async fn send_with_partitioner(
3613        answers: Vec<MetadataAnswer>,
3614        max_block: Duration,
3615        record: ProducerRecord,
3616        partitioner: Option<Arc<dyn Partitioner>>,
3617    ) -> SendOutcome {
3618        let port = Arc::new(AtomicU16::new(0));
3619        let handler_port = Arc::clone(&port);
3620        let metadata_requests = AtomicUsize::new(0);
3621        let metadata_log = Arc::new(Mutex::new(BTreeSet::new()));
3622        let handler_metadata_log = Arc::clone(&metadata_log);
3623        let produce_log = Arc::new(Mutex::new(Vec::new()));
3624        let handler_produce_log = Arc::clone(&produce_log);
3625        let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3626            if api_key == api_versions_request::API_KEY {
3627                return Some(encode_v0(&ApiVersionsResponse {
3628                    api_keys: vec![
3629                        ApiVersion {
3630                            api_key: metadata_request::API_KEY,
3631                            min_version: 0,
3632                            max_version: 12,
3633                            ..Default::default()
3634                        },
3635                        ApiVersion {
3636                            api_key: produce_request::API_KEY,
3637                            min_version: PRODUCE_VERSION,
3638                            max_version: PRODUCE_VERSION,
3639                            ..Default::default()
3640                        },
3641                    ],
3642                    ..Default::default()
3643                }));
3644            }
3645            if api_key == metadata_request::API_KEY {
3646                let index = metadata_requests.fetch_add(1, Ordering::SeqCst);
3647                let answer = answers[index.min(answers.len() - 1)];
3648                let request = requested_topics(body, version);
3649                let topics = request.topics.map(|topics| {
3650                    topics
3651                        .into_iter()
3652                        .map(|topic| topic.name.unwrap_or_default())
3653                        .collect::<Vec<_>>()
3654                });
3655                let named = topics
3656                    .as_ref()
3657                    .is_some_and(|topics| topics.iter().any(|topic| topic == METADATA_TOPIC));
3658                handler_metadata_log
3659                    .lock()
3660                    .unwrap()
3661                    .insert((topics, request.allow_auto_topic_creation));
3662                return metadata_answer(
3663                    version,
3664                    handler_port.load(Ordering::SeqCst),
3665                    answer,
3666                    named,
3667                );
3668            }
3669            if api_key == produce_request::API_KEY {
3670                let mut request_body = &body[2 + CLIENT_ID.len()..];
3671                let request =
3672                    ProduceRequest::decode(&mut request_body, version).expect("decode Produce");
3673                handler_produce_log.lock().unwrap().extend(
3674                    request
3675                        .topic_data
3676                        .iter()
3677                        .flat_map(|topic| topic.partition_data.iter().map(|p| p.index)),
3678                );
3679                return Some(produce_answer(&request));
3680            }
3681            None
3682        })
3683        .await
3684        .unwrap();
3685        port.store(mock.addr.port(), Ordering::SeqCst);
3686        let producer = Producer::builder()
3687            .bootstrap(mock.addr.to_string())
3688            .client_id(CLIENT_ID)
3689            .enable_idempotence(false)
3690            .request_timeout(Duration::from_millis(300))
3691            // Without rebootstrap, a silent `Metadata` fails the refresh, and
3692            // the producer's own wait must ask again.
3693            .metadata_recovery_strategy(krabka_client_core::MetadataRecoveryStrategy::None)
3694            .max_block(max_block)
3695            .maybe_partitioner(partitioner)
3696            .build()
3697            .await
3698            .expect("producer connects to mock broker");
3699        let delivered = producer
3700            .send(record)
3701            .await
3702            .map(|metadata| metadata.partition)
3703            .map_err(|error| error.to_string());
3704        producer.flush().await.expect("flush");
3705        let outcome = SendOutcome {
3706            delivered,
3707            produced_partitions: produce_log.lock().unwrap().clone(),
3708            metadata_requests: metadata_log.lock().unwrap().clone(),
3709        };
3710        producer
3711            .close()
3712            .await
3713            .map(|()| {
3714                mock.stop();
3715                outcome
3716            })
3717            .expect("close producer")
3718    }
3719
3720    /// `send` waits for metadata that holds the topic, as Kafka's
3721    /// `KafkaProducer.waitOnMetadata` does. It never picks a partition from a
3722    /// guessed partition count. A send that fails sends no Produce.
3723    ///
3724    /// The broker is a real socket, so the test runs in real time: paused
3725    /// Tokio time moves on while socket I/O is pending. The unit tests of
3726    /// `metadata_wait` check the backoff and the limit in paused time.
3727    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3728    async fn send_waits_for_topic_metadata_up_to_max_block() {
3729        const UNKNOWN_TOPIC_OR_PARTITION: i16 = 3;
3730        const TOPIC_AUTHORIZATION_FAILED: i16 = 29;
3731        let keyed = |key: &'static [u8]| ProducerRecord {
3732            topic: METADATA_TOPIC.into(),
3733            key: Some(Bytes::from_static(key)),
3734            value: Some(Bytes::from_static(b"v")),
3735            ..Default::default()
3736        };
3737        let pinned = |partition: i32| ProducerRecord {
3738            topic: METADATA_TOPIC.into(),
3739            partition: Some(partition),
3740            value: Some(Bytes::from_static(b"v")),
3741            ..Default::default()
3742        };
3743        let topic = |error_code: i16, partitions: i32| MetadataAnswer::Topic {
3744            error_code,
3745            partitions,
3746        };
3747        // Kafka's `ProducerMetadata.newMetadataRequestBuilder` names the
3748        // producer's topics and allows auto topic creation.
3749        let orders_request = || maplit::btreeset! {(Some(vec![METADATA_TOPIC.to_owned()]), true)};
3750        let delivered = |partition: i32| SendOutcome {
3751            delivered: Ok(partition),
3752            produced_partitions: vec![partition],
3753            metadata_requests: orders_request(),
3754        };
3755        let failed = |error: &str| SendOutcome {
3756            delivered: Err(error.to_owned()),
3757            produced_partitions: vec![],
3758            metadata_requests: orders_request(),
3759        };
3760        let default_block = DEFAULT_PRODUCER_MAX_BLOCK;
3761        let short_block = Duration::from_millis(100);
3762        let cases = [
3763            (
3764                "topic appears on the second refresh",
3765                vec![topic(UNKNOWN_TOPIC_OR_PARTITION, 0), topic(0, 12)],
3766                default_block,
3767                keyed(b"kafka"),
3768                delivered(partition_for_key(b"kafka", 12)),
3769            ),
3770            (
3771                "topic never appears",
3772                vec![topic(UNKNOWN_TOPIC_OR_PARTITION, 0)],
3773                short_block,
3774                keyed(b"kafka"),
3775                failed("Topic orders not present in metadata after 100 ms."),
3776            ),
3777            (
3778                "metadata transport error, then the topic",
3779                vec![MetadataAnswer::Silent, topic(0, 12)],
3780                default_block,
3781                keyed(b"my-key"),
3782                delivered(9),
3783            ),
3784            (
3785                "explicit partition beyond the count",
3786                vec![topic(0, 4)],
3787                short_block,
3788                pinned(7),
3789                failed(
3790                    "Partition 7 of topic orders with partition count 4 is not present in metadata after 100 ms.",
3791                ),
3792            ),
3793            (
3794                "explicit partition after the partition count grows",
3795                vec![topic(0, 4), topic(0, 12)],
3796                default_block,
3797                pinned(7),
3798                delivered(7),
3799            ),
3800            (
3801                "topic authorization failed fails at once",
3802                vec![topic(TOPIC_AUTHORIZATION_FAILED, 0)],
3803                default_block,
3804                keyed(b"kafka"),
3805                failed("broker error_code 29"),
3806            ),
3807        ];
3808        let mut actual = Vec::new();
3809        let mut wanted = Vec::new();
3810        for (name, answers, max_block, record, expected) in cases {
3811            let outcome = send_against_scripted_metadata(answers, max_block, record).await;
3812            actual.push((name, outcome));
3813            wanted.push((name, expected));
3814        }
3815        assert2::assert!(actual == wanted);
3816    }
3817
3818    /// What a send gave while the buffer memory was full.
3819    #[derive(Debug, PartialEq, Eq)]
3820    struct BlockedSendOutcome {
3821        /// `send` did not return before the broker answered the earlier
3822        /// batches, or before `max_block` ended.
3823        blocked: bool,
3824        /// The outcome of the record: delivered, the error text, or `pending`.
3825        delivered: Result<(), String>,
3826    }
3827
3828    /// Start a broker with the topic `orders` of three partitions. It answers
3829    /// Produce only while `answer_produce` is set. It gives back the number of
3830    /// Produce requests that it got.
3831    async fn three_partition_broker(
3832        answer_produce: Arc<AtomicBool>,
3833    ) -> (MockBroker, Arc<AtomicUsize>) {
3834        let port = Arc::new(AtomicU16::new(0));
3835        let handler_port = Arc::clone(&port);
3836        let handler_answer = answer_produce;
3837        let produce_requests = Arc::new(AtomicUsize::new(0));
3838        let handler_produce_requests = Arc::clone(&produce_requests);
3839        let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3840            if api_key == api_versions_request::API_KEY {
3841                return Some(encode_v0(&ApiVersionsResponse {
3842                    api_keys: vec![
3843                        ApiVersion {
3844                            api_key: metadata_request::API_KEY,
3845                            min_version: 0,
3846                            max_version: 12,
3847                            ..Default::default()
3848                        },
3849                        ApiVersion {
3850                            api_key: produce_request::API_KEY,
3851                            min_version: PRODUCE_VERSION,
3852                            max_version: PRODUCE_VERSION,
3853                            ..Default::default()
3854                        },
3855                    ],
3856                    ..Default::default()
3857                }));
3858            }
3859            if api_key == metadata_request::API_KEY {
3860                return metadata_answer(
3861                    version,
3862                    handler_port.load(Ordering::SeqCst),
3863                    MetadataAnswer::Topic {
3864                        error_code: 0,
3865                        partitions: 3,
3866                    },
3867                    true,
3868                );
3869            }
3870            if api_key == produce_request::API_KEY {
3871                handler_produce_requests.fetch_add(1, Ordering::SeqCst);
3872            }
3873            if api_key == produce_request::API_KEY && handler_answer.load(Ordering::SeqCst) {
3874                let mut request_body = &body[2 + CLIENT_ID.len()..];
3875                let request =
3876                    ProduceRequest::decode(&mut request_body, version).expect("decode Produce");
3877                return Some(produce_answer(&request));
3878            }
3879            None
3880        })
3881        .await
3882        .unwrap();
3883        port.store(mock.addr.port(), Ordering::SeqCst);
3884        (mock, produce_requests)
3885    }
3886
3887    /// Kafka's `ProducerMetadata.newMetadataRequestBuilder` names the topics
3888    /// that the producer sends to, with `allowAutoTopicCreation` true. The
3889    /// periodic and error refreshes of the client use the same request.
3890    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3891    async fn client_metadata_refreshes_name_the_topics_of_the_sends() {
3892        let named = |topics: &[&str]| {
3893            krabka_client_core::topics_request(topics.iter().map(|&t| t.to_owned()), true)
3894        };
3895        for (name, sends, expected) in [
3896            ("no send", vec![], named(&[])),
3897            ("send to t1", vec!["t1"], named(&["t1"])),
3898            (
3899                "send to t1, then t2",
3900                vec!["t1", "t2", "t1"],
3901                named(&["t1", "t2"]),
3902            ),
3903        ] {
3904            let port = Arc::new(AtomicU16::new(0));
3905            let handler_port = Arc::clone(&port);
3906            let requests = Arc::new(Mutex::new(Vec::new()));
3907            let handler_requests = Arc::clone(&requests);
3908            let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3909                if api_key == api_versions_request::API_KEY {
3910                    return Some(encode_v0(&ApiVersionsResponse {
3911                        api_keys: vec![ApiVersion {
3912                            api_key: metadata_request::API_KEY,
3913                            min_version: 0,
3914                            max_version: 12,
3915                            ..Default::default()
3916                        }],
3917                        ..Default::default()
3918                    }));
3919                }
3920                if api_key != metadata_request::API_KEY {
3921                    return None;
3922                }
3923                let request = requested_topics(body, version);
3924                let topics = request.topics.clone().unwrap_or_default();
3925                handler_requests.lock().unwrap().push(request);
3926                let response = MetadataResponse {
3927                    brokers: vec![MetadataResponseBroker {
3928                        node_id: 1,
3929                        host: "127.0.0.1".into(),
3930                        port: i32::from(handler_port.load(Ordering::SeqCst)),
3931                        ..Default::default()
3932                    }],
3933                    topics: topics
3934                        .into_iter()
3935                        .map(|topic| MetadataResponseTopic {
3936                            name: topic.name,
3937                            partitions: vec![MetadataResponsePartition {
3938                                leader_id: 1,
3939                                ..Default::default()
3940                            }],
3941                            ..Default::default()
3942                        })
3943                        .collect(),
3944                    ..Default::default()
3945                };
3946                let mut buf = BytesMut::new();
3947                if version >= metadata_response::FLEXIBLE_MIN {
3948                    buf.extend_from_slice(&[0]);
3949                }
3950                response.encode(&mut buf, version).expect("encode Metadata");
3951                Some(buf.to_vec())
3952            })
3953            .await
3954            .unwrap();
3955            port.store(mock.addr.port(), Ordering::SeqCst);
3956            let producer = Producer::builder()
3957                .bootstrap(mock.addr.to_string())
3958                .client_id(CLIENT_ID)
3959                .enable_idempotence(false)
3960                .max_block(Duration::from_secs(2))
3961                .build()
3962                .await
3963                .expect("producer connects to mock broker");
3964            for topic in sends {
3965                producer
3966                    .partition_count(topic, None)
3967                    .await
3968                    .expect("topic metadata");
3969            }
3970            producer
3971                .client
3972                .refresh_metadata()
3973                .await
3974                .expect("refresh metadata");
3975            let last = requests.lock().unwrap().last().cloned();
3976            mock.stop();
3977            drop(producer);
3978            assert2::check!(last == Some(expected), "{name}");
3979        }
3980    }
3981
3982    /// What a send of one record gave, and the Produce requests that the
3983    /// broker got.
3984    #[derive(Debug, PartialEq, Eq)]
3985    struct ValidatedSend {
3986        delivered: Result<(), String>,
3987        produce_requests: usize,
3988    }
3989
3990    /// Kafka's `ProducerRecord` constructor rejects a negative timestamp, and
3991    /// then a negative partition, with `IllegalArgumentException`. The record
3992    /// never reaches `send`, so no request goes to the broker.
3993    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3994    async fn send_rejects_a_negative_partition_or_timestamp() {
3995        let cases = [
3996            (
3997                "negative partition",
3998                Some(-1),
3999                None,
4000                ValidatedSend {
4001                    delivered: Err("Invalid partition: -1. Partition number should always be \
4002                                    non-negative or null."
4003                        .to_owned()),
4004                    produce_requests: 0,
4005                },
4006            ),
4007            (
4008                "negative timestamp",
4009                None,
4010                Some(-5),
4011                ValidatedSend {
4012                    delivered: Err("Invalid timestamp: -5. Timestamp should always be \
4013                                    non-negative or null."
4014                        .to_owned()),
4015                    produce_requests: 0,
4016                },
4017            ),
4018            (
4019                "both negative reports the timestamp first",
4020                Some(-2),
4021                Some(-3),
4022                ValidatedSend {
4023                    delivered: Err("Invalid timestamp: -3. Timestamp should always be \
4024                                    non-negative or null."
4025                        .to_owned()),
4026                    produce_requests: 0,
4027                },
4028            ),
4029            (
4030                "partition 0 and timestamp 0",
4031                Some(0),
4032                Some(0),
4033                ValidatedSend {
4034                    delivered: Ok(()),
4035                    produce_requests: 1,
4036                },
4037            ),
4038        ];
4039        for (name, partition, timestamp_ms, expected) in cases {
4040            let (mock, produce_requests) =
4041                three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4042            let producer = Producer::builder()
4043                .bootstrap(mock.addr.to_string())
4044                .client_id(CLIENT_ID)
4045                .enable_idempotence(false)
4046                .max_block(Duration::from_secs(2))
4047                .build()
4048                .await
4049                .expect("producer connects to mock broker");
4050            let receiver = producer.send(ProducerRecord {
4051                topic: METADATA_TOPIC.into(),
4052                partition,
4053                timestamp_ms,
4054                value: Some(Bytes::from_static(b"v")),
4055                ..Default::default()
4056            });
4057            let delivered = time::timeout(Duration::from_secs(5), receiver)
4058                .await
4059                .map_or_else(
4060                    |_| Err("pending".to_owned()),
4061                    |answer| answer.map(drop).map_err(|error| error.to_string()),
4062                );
4063            let actual = ValidatedSend {
4064                delivered,
4065                produce_requests: produce_requests.load(Ordering::SeqCst),
4066            };
4067            mock.stop();
4068            drop(producer);
4069            assert2::assert!(actual == expected, "{name}");
4070        }
4071    }
4072
4073    /// The `Debug` form of a producer names the level of its codec, and no
4074    /// level for a codec without levels.
4075    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4076    async fn debug_names_the_compression_level_of_the_codec() {
4077        let (mock, _) = three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4078        let mut actual = Vec::new();
4079        for compression in [Compression::Gzip, Compression::Zstd, Compression::Snappy] {
4080            let producer = Producer::builder()
4081                .bootstrap(mock.addr.to_string())
4082                .client_id(CLIENT_ID)
4083                .enable_idempotence(false)
4084                .compression(compression)
4085                .compression_gzip_level(5)
4086                .compression_zstd_level(19)
4087                .build()
4088                .await
4089                .expect("producer connects to mock broker");
4090            let debug = format!("{producer:?}");
4091            drop(producer);
4092            actual.push(
4093                debug
4094                    .split(", ")
4095                    .find(|field| field.starts_with("compression_level"))
4096                    .map(str::to_owned),
4097            );
4098        }
4099        mock.stop();
4100        assert2::assert!(
4101            actual
4102                == vec![
4103                    Some("compression_level: Some(5)".to_owned()),
4104                    Some("compression_level: Some(19)".to_owned()),
4105                    Some("compression_level: None".to_owned()),
4106                ]
4107        );
4108    }
4109
4110    /// A record that does not fit the current batch closes that batch before
4111    /// it waits for memory, as Kafka's `RecordAccumulator.append` closes a full
4112    /// batch. The sender then sends the closed batch before its linger ends,
4113    /// and the memory it frees serves the new batch.
4114    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4115    async fn a_record_that_does_not_fit_sends_the_full_batch_to_free_memory() {
4116        let (mock, _) = three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4117        let producer = Producer::builder()
4118            .bootstrap(mock.addr.to_string())
4119            .client_id(CLIENT_ID)
4120            .enable_idempotence(false)
4121            .batch_size(64)
4122            // Kafka's size check counts a record of 40 value bytes as 125
4123            // bytes, so the memory must hold that. Two batches of 64 bytes
4124            // do not fit in it, so the second record still waits for memory.
4125            .buffer_memory(125)
4126            .linger(Duration::from_secs(30))
4127            .max_block(Duration::from_secs(2))
4128            .build()
4129            .await
4130            .expect("producer connects to mock broker");
4131        let record = || ProducerRecord {
4132            topic: METADATA_TOPIC.into(),
4133            partition: Some(0),
4134            value: Some(Bytes::from_static(&[0; 40])),
4135            ..Default::default()
4136        };
4137
4138        let first = producer.enqueue(record()).await.expect("first is queued");
4139        let second = producer.enqueue(record()).await.expect("second is queued");
4140        let delivered = time::timeout(Duration::from_secs(5), async {
4141            let first = first.await.map(drop);
4142            let second = producer.flush().await.and(second.await.map(drop));
4143            (
4144                first.map_err(|e| e.to_string()),
4145                second.map_err(|e| e.to_string()),
4146            )
4147        })
4148        .await;
4149        mock.stop();
4150        drop(producer);
4151
4152        assert2::assert!(delivered == Ok((Ok(()), Ok(()))));
4153    }
4154
4155    /// The limits of one row of
4156    /// `send_fails_a_record_larger_than_max_request_size_or_buffer_memory`.
4157    struct SizeLimits {
4158        max_request_size: Option<usize>,
4159        buffer_memory: usize,
4160    }
4161
4162    /// What one send of a large record gave.
4163    #[derive(Debug, PartialEq, Eq)]
4164    struct LargeRecordOutcome {
4165        /// The value bytes of the record.
4166        value_bytes: usize,
4167        /// Delivered, or the error text.
4168        delivered: Result<(), String>,
4169        /// The Produce requests that the broker got for the record.
4170        produce_requests: usize,
4171    }
4172
4173    /// Kafka's `KafkaProducer.ensureValidRecordSize` fails a record whose
4174    /// serialized size is larger than `max.request.size`, and then one that
4175    /// is larger than `buffer.memory`, before the accumulator. The size is
4176    /// Kafka's upper bound: 61 bytes of batch header, 21 bytes of record
4177    /// overhead, one byte for the null key, the varint length and the value,
4178    /// and one byte for the header count.
4179    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4180    async fn send_fails_a_record_larger_than_max_request_size_or_buffer_memory() {
4181        const MIB: usize = 1024 * 1024;
4182        let max_request_error = |size: usize, limit: usize| {
4183            format!(
4184                "The message is {size} bytes when serialized which is larger than {limit}, \
4185                 which is the value of the max_request_size configuration."
4186            )
4187        };
4188        let buffer_memory_error = |size: usize| {
4189            format!(
4190                "The message is {size} bytes when serialized which is larger than the total \
4191                 memory buffer you have configured with the buffer_memory configuration."
4192            )
4193        };
4194        let default_limits = || SizeLimits {
4195            max_request_size: None,
4196            buffer_memory: 32 * MIB,
4197        };
4198        // A value of 1048489 bytes has a 3-byte varint length, so the record
4199        // is 87 + 1048489 = 1048576 bytes. A value of 938 bytes has a 2-byte
4200        // varint length, so the record is 86 + 938 = 1024 bytes.
4201        let cases = [
4202            (
4203                "record of max_request_size bytes, default limit",
4204                default_limits(),
4205                1_048_489,
4206                LargeRecordOutcome {
4207                    value_bytes: 1_048_489,
4208                    delivered: Ok(()),
4209                    produce_requests: 1,
4210                },
4211            ),
4212            (
4213                "one byte over max_request_size, default limit",
4214                default_limits(),
4215                1_048_490,
4216                LargeRecordOutcome {
4217                    value_bytes: 1_048_490,
4218                    delivered: Err(max_request_error(1_048_577, MIB)),
4219                    produce_requests: 0,
4220                },
4221            ),
4222            (
4223                "record of buffer_memory bytes",
4224                SizeLimits {
4225                    max_request_size: None,
4226                    buffer_memory: 1024,
4227                },
4228                938,
4229                LargeRecordOutcome {
4230                    value_bytes: 938,
4231                    delivered: Ok(()),
4232                    produce_requests: 1,
4233                },
4234            ),
4235            (
4236                "one byte over buffer_memory",
4237                SizeLimits {
4238                    max_request_size: None,
4239                    buffer_memory: 1024,
4240                },
4241                939,
4242                LargeRecordOutcome {
4243                    value_bytes: 939,
4244                    delivered: Err(buffer_memory_error(1025)),
4245                    produce_requests: 0,
4246                },
4247            ),
4248            (
4249                "over both limits reports max_request_size first",
4250                SizeLimits {
4251                    max_request_size: Some(500),
4252                    buffer_memory: 400,
4253                },
4254                939,
4255                LargeRecordOutcome {
4256                    value_bytes: 939,
4257                    delivered: Err(max_request_error(1025, 500)),
4258                    produce_requests: 0,
4259                },
4260            ),
4261            (
4262                "a larger max_request_size accepts a larger record",
4263                SizeLimits {
4264                    max_request_size: Some(2 * MIB),
4265                    buffer_memory: 32 * MIB,
4266                },
4267                1_048_490,
4268                LargeRecordOutcome {
4269                    value_bytes: 1_048_490,
4270                    delivered: Ok(()),
4271                    produce_requests: 1,
4272                },
4273            ),
4274        ];
4275
4276        let mut actual = Vec::new();
4277        let mut wanted = Vec::new();
4278        for (name, limits, value_bytes, expected) in cases {
4279            let (mock, produce_count) =
4280                three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4281            let producer = Producer::builder()
4282                .bootstrap(mock.addr.to_string())
4283                .client_id(CLIENT_ID)
4284                .enable_idempotence(false)
4285                .batch_size(64)
4286                .buffer_memory(limits.buffer_memory)
4287                .maybe_max_request_size(limits.max_request_size)
4288                .build()
4289                .await
4290                .expect("producer connects to mock broker");
4291            let delivered = producer
4292                .send(ProducerRecord {
4293                    topic: METADATA_TOPIC.into(),
4294                    partition: Some(0),
4295                    value: Some(Bytes::from(vec![0; value_bytes])),
4296                    ..Default::default()
4297                })
4298                .await
4299                .map(drop)
4300                .map_err(|error| error.to_string());
4301            mock.stop();
4302            drop(producer);
4303            actual.push((
4304                name,
4305                LargeRecordOutcome {
4306                    value_bytes,
4307                    delivered,
4308                    produce_requests: produce_count.load(Ordering::SeqCst),
4309                },
4310            ));
4311            wanted.push((name, expected));
4312        }
4313        assert2::assert!(actual == wanted);
4314    }
4315
4316    /// Fill the buffer memory with two batches of 16 MiB for partitions 0 and
4317    /// 1, which the broker does not answer, and send a third record to
4318    /// partition 2. When `answer_after` is set, the broker starts to answer
4319    /// Produce after that time, so the first batches complete and free their
4320    /// memory.
4321    async fn send_with_full_buffer_memory(
4322        answer_after: Option<Duration>,
4323        max_block: Duration,
4324    ) -> BlockedSendOutcome {
4325        const BATCH_BYTES: usize = 16 * 1024 * 1024;
4326        let answer_produce = Arc::new(AtomicBool::new(false));
4327        let (mock, _) = three_partition_broker(Arc::clone(&answer_produce)).await;
4328        let producer = Producer::builder()
4329            .bootstrap(mock.addr.to_string())
4330            .client_id(CLIENT_ID)
4331            .enable_idempotence(false)
4332            .batch_size(BATCH_BYTES)
4333            .request_timeout(Duration::from_millis(300))
4334            .retry_backoff(Duration::from_millis(20))
4335            .max_block(max_block)
4336            .build()
4337            .await
4338            .expect("producer connects to mock broker");
4339        let pinned = |partition: i32| ProducerRecord {
4340            topic: METADATA_TOPIC.into(),
4341            partition: Some(partition),
4342            value: Some(Bytes::from_static(b"v")),
4343            ..Default::default()
4344        };
4345        let _first = producer.enqueue(pinned(0)).await.expect("first is queued");
4346        let _second = producer.enqueue(pinned(1)).await.expect("second is queued");
4347        if let Some(delay) = answer_after {
4348            tokio::spawn(async move {
4349                time::sleep(delay).await;
4350                answer_produce.store(true, Ordering::SeqCst);
4351            });
4352        }
4353        let started = Instant::now();
4354        let third = producer.enqueue(pinned(2)).await;
4355        let blocked = started.elapsed() >= Duration::from_millis(80);
4356        let delivered = match third {
4357            Err(error) => Err(error.to_string()),
4358            Ok(handle) => time::timeout(Duration::from_secs(3), handle)
4359                .await
4360                .map_or_else(
4361                    |_| Err("pending".to_owned()),
4362                    |result| result.map(drop).map_err(|error| error.to_string()),
4363                ),
4364        };
4365        mock.stop();
4366        drop(producer);
4367        BlockedSendOutcome { blocked, delivered }
4368    }
4369
4370    /// Kafka's `BufferPool.allocate` blocks a send while `buffer.memory` is in
4371    /// use, for at most the rest of `max.block.ms`. It fails the record with
4372    /// `BufferExhaustedException` when that time ends.
4373    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4374    async fn send_waits_for_buffer_memory_up_to_max_block() {
4375        let cases = [
4376            (
4377                "memory stays full",
4378                None,
4379                Duration::from_millis(200),
4380                BlockedSendOutcome {
4381                    blocked: true,
4382                    delivered: Err(
4383                        "Failed to allocate 16777216 bytes within the configured max \
4384                                    blocking time 200 ms. Total memory: 33554432 bytes. Available \
4385                                    memory: 0 bytes. Poolable size: 16777216 bytes"
4386                            .to_owned(),
4387                    ),
4388                },
4389            ),
4390            (
4391                "earlier batches complete",
4392                Some(Duration::from_millis(100)),
4393                Duration::from_secs(5),
4394                BlockedSendOutcome {
4395                    blocked: true,
4396                    delivered: Ok(()),
4397                },
4398            ),
4399        ];
4400        let mut actual = Vec::new();
4401        let mut wanted = Vec::new();
4402        for (name, answer_after, max_block, expected) in cases {
4403            actual.push((
4404                name,
4405                send_with_full_buffer_memory(answer_after, max_block).await,
4406            ));
4407            wanted.push((name, expected));
4408        }
4409        assert2::assert!(actual == wanted);
4410    }
4411
4412    /// The producer sends a coordinator request again only when the request
4413    /// can have reached the broker. A failure before the send repeats on every
4414    /// attempt.
4415    #[test]
4416    fn only_a_transport_failure_can_have_reached_the_broker() {
4417        use krabka_client_core::ClientError;
4418        use krabka_units::secs;
4419        for (name, error, expected) in [
4420            ("closed connection", ClientError::Disconnected, true),
4421            ("request timeout", ClientError::Timeout(secs(1)), true),
4422            (
4423                "io failure",
4424                ClientError::Io(io::Error::other("broken pipe")),
4425                true,
4426            ),
4427            (
4428                "incompatible version",
4429                ClientError::IncompatibleVersion {
4430                    api_key: 25,
4431                    broker_min: 4,
4432                    broker_max: 5,
4433                    client_min: 0,
4434                    client_max: 3,
4435                },
4436                false,
4437            ),
4438            (
4439                "no coordinator",
4440                ClientError::NoCoordinator {
4441                    key: "group-a".into(),
4442                },
4443                false,
4444            ),
4445        ] {
4446            assert2::assert!(
4447                request_may_have_reached_the_broker(&error) == expected,
4448                "{name}"
4449            );
4450        }
4451    }
4452
4453    /// Start a broker with the topic `orders`, whose partition `p` has the
4454    /// leader `leaders[p]`. A leader of -1 means no leader. It answers every
4455    /// Produce with success, and logs the partition of each Produce.
4456    async fn leader_broker(leaders: &'static [i32]) -> (MockBroker, Arc<Mutex<Vec<i32>>>) {
4457        let port = Arc::new(AtomicU16::new(0));
4458        let handler_port = Arc::clone(&port);
4459        let produce_log = Arc::new(Mutex::new(Vec::new()));
4460        let handler_produce_log = Arc::clone(&produce_log);
4461        let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
4462            if api_key == api_versions_request::API_KEY {
4463                return Some(encode_v0(&ApiVersionsResponse {
4464                    api_keys: vec![
4465                        ApiVersion {
4466                            api_key: metadata_request::API_KEY,
4467                            min_version: 0,
4468                            max_version: 12,
4469                            ..Default::default()
4470                        },
4471                        ApiVersion {
4472                            api_key: produce_request::API_KEY,
4473                            min_version: PRODUCE_VERSION,
4474                            max_version: PRODUCE_VERSION,
4475                            ..Default::default()
4476                        },
4477                    ],
4478                    ..Default::default()
4479                }));
4480            }
4481            if api_key == metadata_request::API_KEY {
4482                let response = MetadataResponse {
4483                    brokers: vec![MetadataResponseBroker {
4484                        node_id: 1,
4485                        host: "127.0.0.1".into(),
4486                        port: i32::from(handler_port.load(Ordering::SeqCst)),
4487                        ..Default::default()
4488                    }],
4489                    topics: vec![MetadataResponseTopic {
4490                        name: Some(METADATA_TOPIC.into()),
4491                        partitions: (0..)
4492                            .zip(leaders)
4493                            .map(|(partition_index, &leader_id)| MetadataResponsePartition {
4494                                partition_index,
4495                                leader_id,
4496                                ..Default::default()
4497                            })
4498                            .collect(),
4499                        ..Default::default()
4500                    }],
4501                    ..Default::default()
4502                };
4503                let mut buf = BytesMut::new();
4504                if version >= metadata_response::FLEXIBLE_MIN {
4505                    buf.extend_from_slice(&[0]);
4506                }
4507                response.encode(&mut buf, version).expect("encode Metadata");
4508                return Some(buf.to_vec());
4509            }
4510            if api_key == produce_request::API_KEY {
4511                let mut request_body = &body[2 + CLIENT_ID.len()..];
4512                let request =
4513                    ProduceRequest::decode(&mut request_body, version).expect("decode Produce");
4514                handler_produce_log.lock().unwrap().extend(
4515                    request
4516                        .topic_data
4517                        .iter()
4518                        .flat_map(|topic| topic.partition_data.iter().map(|p| p.index)),
4519                );
4520                return Some(produce_answer(&request));
4521            }
4522            None
4523        })
4524        .await
4525        .unwrap();
4526        port.store(mock.addr.port(), Ordering::SeqCst);
4527        (mock, produce_log)
4528    }
4529
4530    /// Where the keyless records of one row went.
4531    #[derive(Debug, PartialEq, Eq)]
4532    struct KeylessSpread {
4533        /// The number of distinct partitions of the keyless records.
4534        keyless_partitions: usize,
4535        /// A keyless record went to a partition with no leader.
4536        keyless_on_a_partition_without_leader: bool,
4537        /// The partition of the keyed record of the row, if it has one.
4538        keyed_partition: Option<i32>,
4539        /// Each Produce carried one record.
4540        produce_requests: usize,
4541    }
4542
4543    /// Kafka's `BuiltInPartitioner` (KIP-480, KIP-794) keeps keyless records
4544    /// on one partition until about `batch.size` bytes go there. A drain does
4545    /// not move it, a keyed record does not move it, and it picks only
4546    /// partitions with a leader. Each record here waits for its ack, so each
4547    /// drain sends one record, and the records are much smaller than
4548    /// `batch.size`.
4549    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4550    async fn keyless_records_stay_on_one_partition_with_a_leader() {
4551        const KEYED: &[u8] = b"kafka";
4552        let mut actual_rows = Vec::new();
4553        let mut expected_rows = Vec::new();
4554        for (name, leaders, keyed_at, expected) in [
4555            (
4556                "drains do not move the sticky partition",
4557                &[1, 1, 1, 1][..],
4558                None,
4559                KeylessSpread {
4560                    keyless_partitions: 1,
4561                    keyless_on_a_partition_without_leader: false,
4562                    keyed_partition: None,
4563                    produce_requests: 6,
4564                },
4565            ),
4566            (
4567                "partitions without a leader are skipped",
4568                &[1, -1, -1, 1][..],
4569                None,
4570                KeylessSpread {
4571                    keyless_partitions: 1,
4572                    keyless_on_a_partition_without_leader: false,
4573                    keyed_partition: None,
4574                    produce_requests: 6,
4575                },
4576            ),
4577            (
4578                "a keyed record does not move the sticky partition",
4579                &[1, 1, 1, 1][..],
4580                Some(2),
4581                KeylessSpread {
4582                    keyless_partitions: 1,
4583                    keyless_on_a_partition_without_leader: false,
4584                    keyed_partition: Some(partition_for_key(KEYED, 4)),
4585                    produce_requests: 6,
4586                },
4587            ),
4588        ] {
4589            let closed = {
4590                let (mock, produce_log) = leader_broker(leaders).await;
4591                let producer = Producer::builder()
4592                    .bootstrap(mock.addr.to_string())
4593                    .client_id(CLIENT_ID)
4594                    .enable_idempotence(false)
4595                    .linger(Duration::ZERO)
4596                    .build()
4597                    .await
4598                    .expect("producer connects to mock broker");
4599                let mut keyless = BTreeSet::new();
4600                let mut keyed_partition = None;
4601                for index in 0..6 {
4602                    let key = (keyed_at == Some(index)).then_some(Bytes::from_static(KEYED));
4603                    let partition = producer
4604                        .send(ProducerRecord {
4605                            topic: METADATA_TOPIC.into(),
4606                            key: key.clone(),
4607                            value: Some(Bytes::from_static(b"value")),
4608                            ..Default::default()
4609                        })
4610                        .await
4611                        .expect("the record is delivered")
4612                        .partition;
4613                    if key.is_some() {
4614                        keyed_partition = Some(partition);
4615                    } else {
4616                        keyless.insert(partition);
4617                    }
4618                }
4619                let actual = KeylessSpread {
4620                    keyless_partitions: keyless.len(),
4621                    keyless_on_a_partition_without_leader: keyless.iter().any(|&partition| {
4622                        usize::try_from(partition).is_ok_and(|index| leaders[index] < 0)
4623                    }),
4624                    keyed_partition,
4625                    produce_requests: produce_log.lock().unwrap().len(),
4626                };
4627                producer.close().await.map(|()| {
4628                    mock.stop();
4629                    actual
4630                })
4631            };
4632            actual_rows.push((name, closed.expect("close producer")));
4633            expected_rows.push((name, expected));
4634        }
4635        assert2::assert!(actual_rows == expected_rows);
4636    }
4637}
4638
4639#[cfg(test)]
4640mod transport_configuration_tests {
4641    use std::time::Duration;
4642
4643    use assert2::assert;
4644    use bytes::BytesMut;
4645    use krabka_client_core::{ClientDnsLookup, MockBroker};
4646    use krabka_protocol::{
4647        Encode,
4648        owned::{api_versions_request, api_versions_response::ApiVersionsResponse},
4649    };
4650    use krabka_units::{millis, secs};
4651
4652    use super::{Acks, Producer, ProducerError};
4653
4654    #[tokio::test]
4655    async fn producer_retains_transport_overrides_in_primary_and_coordinator_clients()
4656    -> Result<(), ProducerError> {
4657        let mock = MockBroker::start(|api, _, _, _| {
4658            if api != api_versions_request::API_KEY {
4659                return None;
4660            }
4661            let mut body = BytesMut::new();
4662            ApiVersionsResponse::default()
4663                .encode(&mut body, 0)
4664                .expect("encode ApiVersions");
4665            Some(body.to_vec())
4666        })
4667        .await
4668        .unwrap();
4669        let producer = Producer::builder()
4670            .bootstrap(mock.addr.to_string())
4671            .request_timeout(Duration::from_millis(10))
4672            .acks(Acks::Zero)
4673            .enable_idempotence(false)
4674            .enable_metrics_push(false)
4675            .dns_timeout(millis(95))
4676            .client_dns_lookup(ClientDnsLookup::UseAllDnsIps)
4677            .reconnect_backoff(millis(37))
4678            .reconnect_backoff_max(millis(137))
4679            .connections_max_idle(secs(67))
4680            .socket_connection_setup_timeout(secs(17))
4681            .socket_connection_setup_timeout_max(secs(27))
4682            .build()
4683            .await
4684            .expect("producer");
4685        let coordinator = producer
4686            .connect_txn_coordinator(mock.addr.to_string())
4687            .await
4688            .expect("coordinator");
4689        for options in [
4690            producer.client.connection_options(),
4691            coordinator.connection_options(),
4692        ] {
4693            assert!(
4694                (
4695                    options.dns_timeout.time(),
4696                    options.client_dns_lookup,
4697                    options.reconnect_backoff,
4698                    options.reconnect_backoff_max,
4699                    options.connections_max_idle,
4700                    options.socket_connection_setup_timeout,
4701                    options.socket_connection_setup_timeout_max
4702                ) == (
4703                    millis(95),
4704                    ClientDnsLookup::UseAllDnsIps,
4705                    millis(37),
4706                    millis(137),
4707                    secs(67),
4708                    secs(17),
4709                    secs(27)
4710                )
4711            );
4712        }
4713        assert!(producer.client.refresh_metadata().await.is_err());
4714        coordinator.close();
4715        producer.close().await.map(|()| mock.stop())
4716    }
4717}