Skip to main content

krabka_client_producer/
builder.rs

1//! `Producer::builder()`: the `bon`-generated builder for `Producer::start`.
2
3use std::{
4    sync::{
5        Arc,
6        atomic::{AtomicBool, AtomicI16, AtomicI64, AtomicU8, AtomicU64, AtomicUsize, Ordering},
7    },
8    time::Duration,
9};
10
11use bytes::BufMut;
12use dashmap::DashMap;
13use krabka_client_core::{
14    Client, ClientDnsTimeout, ClientError, ClientFrameMax, ConnectionDispatchQueueCapacity,
15    DEFAULT_CLIENT_DNS_TIMEOUT, DEFAULT_CLIENT_FRAME_MAX,
16    DEFAULT_CONNECTION_DISPATCH_QUEUE_CAPACITY, security::ClientSecurity, telemetry,
17};
18use krabka_protocol::{
19    Encode, ProtocolError, ProtocolRequest,
20    owned::{
21        init_producer_id_request::{self, InitProducerIdRequest},
22        init_producer_id_response::InitProducerIdResponse,
23    },
24};
25use krabka_units::{
26    ByteSize, Time,
27    convert::{StdDurationExt as _, TimeExt as _},
28};
29use refined_type::rule::{GreaterI32, GreaterUsize, MinMaxU128, MinMaxUsize};
30use tokio::{
31    sync::{Mutex, Notify, mpsc},
32    task::JoinHandle,
33    time::{self, Instant},
34};
35use tokio_util::sync::CancellationToken;
36
37use crate::{
38    accumulator::AccumulatorMap,
39    buffer_pool::BufferPool,
40    compression::{
41        Compression, CompressionLevels, DEFAULT_PRODUCER_COMPRESSION_GZIP_LEVEL,
42        DEFAULT_PRODUCER_COMPRESSION_LZ4_LEVEL, DEFAULT_PRODUCER_COMPRESSION_ZSTD_LEVEL,
43    },
44    error::ProducerError,
45    metadata_age::{
46        DEFAULT_PRODUCER_METADATA_MAX_AGE, DEFAULT_PRODUCER_METADATA_MAX_IDLE, MetadataAge,
47        validate_metadata_max_idle,
48    },
49    metadata_wait,
50    partitioner::{BuiltInPartitioner, Partitioner, PartitionerConfig},
51    producer,
52    producer::{Acks, Producer, ProducerIdentity, SocketBuffers},
53    sender,
54    transactional::{PreparedTransactionState, TxnErrorSlot, TxnState},
55    transport::ClientTransport,
56    txn_retry::{self, CoordinatorAttempt, TxnRequestDecision},
57};
58
59/// The first `InitProducerId` version that holds the two-phase-commit fields.
60const INIT_PRODUCER_ID_2PC_MIN_VERSION: i16 = 6;
61/// The last released `InitProducerId` version.
62const INIT_PRODUCER_ID_STABLE_MAX_VERSION: i16 = 5;
63
64/// An `InitProducerId` request that negotiates released versions only.
65///
66/// `InitProducerIdRequest.json` marks v6 (KIP-939) as
67/// `"latestVersionUnstable": true`. Apache Kafka's producer builds the request
68/// with `InitProducerIdRequest.Builder(data)`, which calls
69/// `AbstractRequest.Builder(ApiKeys.INIT_PRODUCER_ID)`. That constructor uses
70/// `latestVersion(false)`, which leaves out the unstable v6. The generated
71/// `InitProducerIdRequest` allows v6, so this type gives the cap to version
72/// negotiation. A request that needs the two-phase commit fields still sends
73/// v6.
74#[derive(Clone, Debug, PartialEq, Eq)]
75struct StableInitProducerId(InitProducerIdRequest);
76
77impl Encode for StableInitProducerId {
78    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
79        self.0.encode(buf, version)
80    }
81
82    fn encoded_len(&self, version: i16) -> usize {
83        self.0.encoded_len(version)
84    }
85}
86
87impl ProtocolRequest for StableInitProducerId {
88    const API_KEY: i16 = init_producer_id_request::API_KEY;
89    const MIN_VERSION: i16 = init_producer_id_request::MIN_VERSION;
90    const MAX_VERSION: i16 = INIT_PRODUCER_ID_STABLE_MAX_VERSION;
91    /// The cap is a released version, so it is also the stable maximum.
92    const LATEST_STABLE_VERSION: i16 = Self::MAX_VERSION;
93    const FLEXIBLE_MIN: i16 = init_producer_id_request::FLEXIBLE_MIN;
94    type Response = InitProducerIdResponse;
95}
96
97/// An `InitProducerId` request with the two-phase commit fields, which only
98/// v6 carries.
99///
100/// Kafka marks v6 `latestVersionUnstable`, so version negotiation leaves it
101/// out of the generated `InitProducerIdRequest`. A two-phase commit request
102/// has no stable version, and this type keeps v6 negotiable for it.
103#[derive(Clone, Debug, PartialEq, Eq)]
104struct TwoPhaseCommitInitProducerId(InitProducerIdRequest);
105
106impl Encode for TwoPhaseCommitInitProducerId {
107    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
108        self.0.encode(buf, version)
109    }
110
111    fn encoded_len(&self, version: i16) -> usize {
112        self.0.encoded_len(version)
113    }
114}
115
116impl ProtocolRequest for TwoPhaseCommitInitProducerId {
117    const API_KEY: i16 = init_producer_id_request::API_KEY;
118    const MIN_VERSION: i16 = INIT_PRODUCER_ID_2PC_MIN_VERSION;
119    const MAX_VERSION: i16 = init_producer_id_request::MAX_VERSION;
120    const LATEST_STABLE_VERSION: i16 = Self::MAX_VERSION;
121    const FLEXIBLE_MIN: i16 = init_producer_id_request::FLEXIBLE_MIN;
122    type Response = InitProducerIdResponse;
123}
124
125/// Default producer compression.
126pub const DEFAULT_PRODUCER_COMPRESSION: Compression = Compression::None;
127/// Default delay before sending a partial producer batch.
128///
129/// Kafka's `linger.ms` default is 5 (KIP-1030).
130pub const DEFAULT_PRODUCER_LINGER: Duration = Duration::from_millis(5);
131/// Default producer acknowledgement mode. Kafka's `acks` default is `all`.
132pub const DEFAULT_PRODUCER_ACKS: Acks = Acks::All;
133/// The largest `max_in_flight_per_connection` an idempotent producer accepts.
134///
135/// Kafka's `ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION_FOR_IDEMPOTENCE`.
136const MAX_IN_FLIGHT_FOR_IDEMPOTENCE: usize = 5;
137
138/// The number of the next generated `producer-<n>` client id.
139///
140/// Kafka's `ProducerConfig.PRODUCER_CLIENT_ID_SEQUENCE` starts at 1.
141static PRODUCER_CLIENT_ID_SEQUENCE: AtomicU64 = AtomicU64::new(1);
142/// Default producer batch size in bytes.
143pub const DEFAULT_PRODUCER_BATCH_BYTES: usize = 16 * 1024;
144/// Default of `partitioner_ignore_keys`. Kafka's `partitioner.ignore.keys`
145/// default is `false`.
146pub const DEFAULT_PRODUCER_PARTITIONER_IGNORE_KEYS: bool = false;
147/// Default of `partitioner_adaptive_partitioning_enable`. Kafka's
148/// `partitioner.adaptive.partitioning.enable` default is `true`.
149pub const DEFAULT_PRODUCER_PARTITIONER_ADAPTIVE_PARTITIONING_ENABLE: bool = true;
150/// Default of `partitioner_availability_timeout`. Kafka's
151/// `partitioner.availability.timeout.ms` default is 0, which turns the check
152/// off.
153pub const DEFAULT_PRODUCER_PARTITIONER_AVAILABILITY_TIMEOUT: Duration = Duration::ZERO;
154/// Default of `partitioner_rack_aware`. Kafka's `partitioner.rack.aware`
155/// default is `false`.
156pub const DEFAULT_PRODUCER_PARTITIONER_RACK_AWARE: bool = false;
157/// Default cross-partition in-flight request limit.
158pub const DEFAULT_PRODUCER_MAX_IN_FLIGHT: usize = 5;
159/// Default producer request timeout.
160pub const DEFAULT_PRODUCER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
161/// Default deadline for flushing all buffered and in-flight records.
162pub const DEFAULT_PRODUCER_FLUSH_TIMEOUT: Duration = Duration::from_secs(50);
163/// Default retries after a batch's initial send.
164pub const DEFAULT_PRODUCER_RETRIES: i32 = i32::MAX;
165/// Default producer retry backoff.
166pub const DEFAULT_PRODUCER_RETRY_BACKOFF: Duration = Duration::from_millis(100);
167/// Default producer delivery timeout. Kafka's `delivery.timeout.ms` default is
168/// 120000.
169pub const DEFAULT_PRODUCER_DELIVERY_TIMEOUT: Duration = Duration::from_mins(2);
170/// Default producer-ID initialization retry timeout.
171///
172/// The same timeout limits the retries of a transaction coordinator request:
173/// `AddPartitionsToTxn`, and an `EndTxn` whose outcome a transport failure hid.
174/// It also limits the `FindCoordinator` retries of each of them, and bounds the
175/// lookup and `InitProducerId` of `init_transactions` together.
176pub const DEFAULT_PRODUCER_INIT_RETRY_TIMEOUT: Duration = Duration::from_secs(30);
177/// Default upper bound of the exponential retry backoff. Kafka's
178/// `retry.backoff.max.ms` default is 1000.
179pub const DEFAULT_PRODUCER_RETRY_BACKOFF_MAX: Duration = Duration::from_secs(1);
180/// Default transaction timeout.
181pub const DEFAULT_PRODUCER_TRANSACTION_TIMEOUT: Duration = Duration::from_mins(1);
182/// Default longest time that `send` waits for the metadata of its topic.
183///
184/// Kafka's `max.block.ms` has the same default of 60 s.
185pub const DEFAULT_PRODUCER_MAX_BLOCK: Duration = Duration::from_mins(1);
186/// Default producer buffer memory in bytes.
187///
188/// Kafka's `buffer.memory` default is 33554432 (32 MiB).
189pub const DEFAULT_PRODUCER_BUFFER_MEMORY: usize = 32 * 1024 * 1024;
190
191/// Default largest serialized record that `send` accepts, in bytes.
192///
193/// Kafka's `max.request.size` default is 1048576 (1 MiB).
194pub const DEFAULT_PRODUCER_MAX_REQUEST_SIZE: usize = 1024 * 1024;
195
196/// Bounded backlog for coalescing internal sender wakeups.
197const SENDER_WAKE_CHANNEL_CAPACITY: usize = 16;
198
199/// Validated deadline for flushing all buffered and in-flight records.
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub struct ProducerFlushTimeout(Duration);
202
203impl ProducerFlushTimeout {
204    /// Validate a producer flush timeout.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error when the timeout is zero, fractional milliseconds, or
209    /// exceeds `i32::MAX` milliseconds.
210    pub fn new(value: Duration) -> Result<Self, String> {
211        validated_protocol_duration(value, "producer flush timeout").map(Self)
212    }
213
214    #[must_use]
215    pub const fn duration(self) -> Duration {
216        self.0
217    }
218
219    #[must_use]
220    pub fn milliseconds(self) -> i32 {
221        protocol_milliseconds(self.0)
222    }
223}
224
225impl Default for ProducerFlushTimeout {
226    /// [`DEFAULT_PRODUCER_FLUSH_TIMEOUT`], which [`Self::new`] accepts.
227    fn default() -> Self {
228        Self(DEFAULT_PRODUCER_FLUSH_TIMEOUT)
229    }
230}
231
232/// Validated producer batching and compression policy.
233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub struct ProducerThroughputPolicy {
235    compression: Compression,
236    linger: Duration,
237    linger_ms: i32,
238    batch_bytes: usize,
239    max_in_flight: usize,
240}
241
242impl ProducerThroughputPolicy {
243    /// Validate producer batching and compression settings.
244    ///
245    /// # Errors
246    ///
247    /// Returns an error when linger is fractional or exceeds `i32::MAX`
248    /// milliseconds, batch bytes are outside `1..=i32::MAX`, or max in flight
249    /// is zero.
250    pub fn new(
251        compression: Compression,
252        linger: Duration,
253        batch_bytes: usize,
254        max_in_flight: usize,
255    ) -> Result<Self, String> {
256        let milliseconds = MinMaxU128::<0, { i32::MAX as u128 }>::new(linger.as_millis())
257            .map_err(|error| format!("producer linger: {error}"))?
258            .into_value();
259        let milliseconds =
260            u64::try_from(milliseconds).map_err(|error| format!("producer linger: {error}"))?;
261        if Duration::from_millis(milliseconds) != linger {
262            return Err("producer linger must be a whole number of milliseconds".to_owned());
263        }
264        let linger_ms =
265            i32::try_from(milliseconds).map_err(|error| format!("producer linger: {error}"))?;
266        let batch_bytes = MinMaxUsize::<1, { i32::MAX as usize }>::new(batch_bytes)
267            .map_err(|error| format!("producer batch bytes: {error}"))?
268            .into_value();
269        let max_in_flight = GreaterUsize::<0>::new(max_in_flight)
270            .map_err(|error| format!("producer max in flight: {error}"))?
271            .into_value();
272        Ok(Self {
273            compression,
274            linger,
275            linger_ms,
276            batch_bytes,
277            max_in_flight,
278        })
279    }
280
281    #[must_use]
282    pub const fn compression(self) -> Compression {
283        self.compression
284    }
285
286    #[must_use]
287    pub const fn linger(self) -> Duration {
288        self.linger
289    }
290
291    #[must_use]
292    pub const fn batch_bytes(self) -> usize {
293        self.batch_bytes
294    }
295
296    #[must_use]
297    pub const fn max_in_flight(self) -> usize {
298        self.max_in_flight
299    }
300
301    #[must_use]
302    pub const fn linger_ms(self) -> i32 {
303        self.linger_ms
304    }
305}
306
307impl Default for ProducerThroughputPolicy {
308    /// The `DEFAULT_PRODUCER_*` settings, which [`Self::new`] accepts.
309    fn default() -> Self {
310        Self {
311            compression: DEFAULT_PRODUCER_COMPRESSION,
312            linger: DEFAULT_PRODUCER_LINGER,
313            linger_ms: protocol_milliseconds(DEFAULT_PRODUCER_LINGER),
314            batch_bytes: DEFAULT_PRODUCER_BATCH_BYTES,
315            max_in_flight: DEFAULT_PRODUCER_MAX_IN_FLIGHT,
316        }
317    }
318}
319
320/// Validated producer retry and transaction timing.
321#[derive(Clone, Copy, Debug, Eq, PartialEq)]
322pub struct ProducerRetryPolicy {
323    request_timeout: Duration,
324    retries: i32,
325    retry_backoff: Duration,
326    retry_backoff_max: Duration,
327    init_retry_timeout: Duration,
328    transaction_timeout: Duration,
329}
330
331impl ProducerRetryPolicy {
332    /// The first backoff of the policy: `retry_backoff`, and never more than
333    /// `retry_backoff_max`. A coordinator retry starts at it.
334    fn first_backoff(&self) -> Time {
335        self.retry_backoff().min(self.retry_backoff_max()).as_time()
336    }
337
338    /// Validate producer retry and transaction timing.
339    ///
340    /// # Errors
341    ///
342    /// Returns an error for non-positive durations, retry durations above
343    /// `i32::MAX` milliseconds, negative retries, or protocol timeouts that are
344    /// not whole milliseconds.
345    ///
346    /// A `retry_backoff` above `retry_backoff_max` is valid. Kafka's
347    /// `ExponentialBackoff` then uses `retry_backoff_max` for every retry.
348    pub fn new(
349        request_timeout: Duration,
350        retries: i32,
351        retry_backoff: Duration,
352        retry_backoff_max: Duration,
353        init_retry_timeout: Duration,
354        transaction_timeout: Duration,
355    ) -> Result<Self, String> {
356        let request_timeout = validated_protocol_duration(request_timeout, "request timeout")?;
357        let retries = GreaterI32::<-1>::new(retries)
358            .map_err(|error| format!("producer retries: {error}"))?
359            .into_value();
360        let retry_backoff = validated_duration(retry_backoff, "producer retry backoff")?;
361        let retry_backoff_max =
362            validated_duration(retry_backoff_max, "producer retry backoff maximum")?;
363        let init_retry_timeout = validated_duration(
364            init_retry_timeout,
365            "producer-ID initialization retry timeout",
366        )?;
367        let transaction_timeout =
368            validated_protocol_duration(transaction_timeout, "transaction timeout")?;
369        Ok(Self {
370            request_timeout,
371            retries,
372            retry_backoff,
373            retry_backoff_max,
374            init_retry_timeout,
375            transaction_timeout,
376        })
377    }
378
379    #[must_use]
380    pub const fn request_timeout(self) -> Duration {
381        self.request_timeout
382    }
383
384    #[must_use]
385    pub const fn retries(self) -> i32 {
386        self.retries
387    }
388
389    #[must_use]
390    pub const fn retry_backoff(self) -> Duration {
391        self.retry_backoff
392    }
393
394    #[must_use]
395    pub const fn init_retry_timeout(self) -> Duration {
396        self.init_retry_timeout
397    }
398
399    #[must_use]
400    pub const fn retry_backoff_max(self) -> Duration {
401        self.retry_backoff_max
402    }
403
404    #[must_use]
405    pub const fn transaction_timeout(self) -> Duration {
406        self.transaction_timeout
407    }
408
409    #[must_use]
410    pub fn request_timeout_ms(self) -> i32 {
411        protocol_milliseconds(self.request_timeout)
412    }
413
414    #[must_use]
415    pub fn transaction_timeout_ms(self) -> i32 {
416        protocol_milliseconds(self.transaction_timeout)
417    }
418}
419
420impl Default for ProducerRetryPolicy {
421    /// The `DEFAULT_PRODUCER_*` timings, which [`Self::new`] accepts.
422    fn default() -> Self {
423        Self {
424            request_timeout: DEFAULT_PRODUCER_REQUEST_TIMEOUT,
425            retries: DEFAULT_PRODUCER_RETRIES,
426            retry_backoff: DEFAULT_PRODUCER_RETRY_BACKOFF,
427            retry_backoff_max: DEFAULT_PRODUCER_RETRY_BACKOFF_MAX,
428            init_retry_timeout: DEFAULT_PRODUCER_INIT_RETRY_TIMEOUT,
429            transaction_timeout: DEFAULT_PRODUCER_TRANSACTION_TIMEOUT,
430        }
431    }
432}
433
434/// Validate `max_request_size`. Kafka defines `max.request.size` as an `INT`
435/// with `atLeast(0)`, so the value must fit an `i32`.
436fn validated_max_request_size(max_request_size: usize) -> Result<usize, ProducerError> {
437    Ok(
438        MinMaxUsize::<0, { i32::MAX as usize }>::new(max_request_size)
439            .map_err(|error| {
440                ProducerError::InvalidConfig(format!("producer max request size: {error}"))
441            })?
442            .into_value(),
443    )
444}
445
446fn validated_duration(value: Duration, name: &str) -> Result<Duration, String> {
447    MinMaxU128::<1, { i32::MAX as u128 * 1_000_000 }>::new(value.as_nanos())
448        .map(|_| value)
449        .map_err(|error| format!("{name}: {error}"))
450}
451
452/// Give the delivery timeout, with the rule of Kafka's
453/// `KafkaProducer.configureDeliveryTimeout`.
454///
455/// The delivery timeout must be at least `linger + request_timeout`. When the
456/// application set it, a smaller value is an error. When it did not, a default
457/// smaller than that sum becomes the sum, with a warning.
458///
459/// # Errors
460///
461/// Returns an error when the configured delivery timeout is out of range or
462/// smaller than `linger + request_timeout`.
463fn resolve_delivery_timeout(
464    delivery_timeout: Option<Duration>,
465    linger: Duration,
466    request_timeout: Duration,
467) -> Result<Duration, String> {
468    let smallest = linger
469        .saturating_add(request_timeout)
470        .min(Duration::from_millis(i32::MAX.unsigned_abs().into()));
471    match delivery_timeout {
472        Some(configured) => {
473            let configured = validated_duration(configured, "delivery timeout")?;
474            if configured < smallest {
475                return Err(
476                    "delivery_timeout should be equal to or larger than linger + request_timeout"
477                        .to_owned(),
478                );
479            }
480            Ok(configured)
481        }
482        None if DEFAULT_PRODUCER_DELIVERY_TIMEOUT < smallest => {
483            tracing::warn!(
484                delivery_timeout = ?smallest,
485                "delivery_timeout should be equal to or larger than linger + request_timeout. \
486                 Setting it to linger + request_timeout."
487            );
488            Ok(smallest)
489        }
490        None => Ok(DEFAULT_PRODUCER_DELIVERY_TIMEOUT),
491    }
492}
493
494fn validated_protocol_duration(value: Duration, name: &str) -> Result<Duration, String> {
495    let milliseconds = MinMaxU128::<1, { i32::MAX as u128 }>::new(value.as_millis())
496        .map_err(|error| format!("{name}: {error}"))?
497        .into_value();
498    let milliseconds = u64::try_from(milliseconds).map_err(|error| format!("{name}: {error}"))?;
499    if Duration::from_millis(milliseconds) != value {
500        return Err(format!("{name} must be a whole number of milliseconds"));
501    }
502    Ok(value)
503}
504
505/// The whole milliseconds of `value`, at most `i32::MAX`. The policy
506/// constructors accept only durations in that range, so the value of a
507/// validated duration is exact.
508fn protocol_milliseconds(value: Duration) -> i32 {
509    i32::try_from(value.as_millis()).unwrap_or(i32::MAX)
510}
511
512// cargo-mutants: protocol-default InitProducerId shape, so `-> Default` is equivalent.
513#[cfg_attr(test, mutants::skip)]
514pub fn build_init_producer_id_request() -> InitProducerIdRequest {
515    InitProducerIdRequest {
516        transactional_id: None,
517        transaction_timeout_ms: 0,
518        ..Default::default()
519    }
520}
521
522fn next_backoff(backoff: Duration, max_backoff: Duration) -> Duration {
523    backoff.saturating_mul(2).min(max_backoff)
524}
525
526/// Decide if the producer is idempotent, with the rules of Kafka's
527/// `ProducerConfig.postProcessAndValidateIdempotenceConfigs`.
528///
529/// `enable_idempotence` is `None` when the application did not set it. Then
530/// idempotence is on, and `retries=0` or an `acks` other than `All` turns it
531/// off. When the application set it to `true`, those settings are errors. An
532/// idempotent producer accepts at most 5 in-flight requests per connection,
533/// and a `transactional_id` requires idempotence.
534///
535/// # Errors
536///
537/// Returns [`ProducerError::InvalidConfig`] with Kafka's `ConfigException`
538/// message for each conflict.
539fn resolve_idempotence(
540    enable_idempotence: Option<bool>,
541    acks: Acks,
542    retries: i32,
543    max_in_flight: usize,
544    transactional_id: Option<&str>,
545) -> Result<bool, ProducerError> {
546    let configured = enable_idempotence.is_some();
547    let enabled = if enable_idempotence.unwrap_or(true) {
548        let retries_disable = retries == 0;
549        if retries_disable {
550            if configured {
551                return Err(ProducerError::InvalidConfig(
552                    "Must set retries to non-zero when using the idempotent producer.".to_owned(),
553                ));
554            }
555            tracing::info!("Idempotence will be disabled because retries is set to 0.");
556        }
557        let acks_disable = acks != Acks::All;
558        if acks_disable {
559            if configured {
560                return Err(ProducerError::InvalidConfig(
561                    "Must set acks to all in order to use the idempotent producer. \
562                     Otherwise we cannot guarantee idempotence."
563                        .to_owned(),
564                ));
565            }
566            tracing::info!(
567                acks = ?acks,
568                "Idempotence will be disabled because acks is not set to all."
569            );
570        }
571        if max_in_flight > MAX_IN_FLIGHT_FOR_IDEMPOTENCE {
572            return Err(ProducerError::InvalidConfig(format!(
573                "To use the idempotent producer, max_in_flight_per_connection must be set to \
574                 at most {MAX_IN_FLIGHT_FOR_IDEMPOTENCE}. Current value is {max_in_flight}."
575            )));
576        }
577        !(retries_disable || acks_disable)
578    } else {
579        false
580    };
581    if !enabled && transactional_id.is_some() {
582        return Err(ProducerError::InvalidConfig(
583            "Cannot set a transactional_id without also enabling idempotence.".to_owned(),
584        ));
585    }
586    Ok(enabled)
587}
588
589/// Give the client id, with the rule of Kafka's
590/// `ProducerConfig.maybeOverrideClientId`.
591///
592/// A configured client id is kept, also when it is empty. Otherwise the id is
593/// `producer-<transactional_id>`, or `producer-<n>` from a process-wide
594/// sequence.
595fn resolve_client_id(client_id: Option<String>, transactional_id: Option<&str>) -> String {
596    client_id.unwrap_or_else(|| {
597        transactional_id.map_or_else(
598            || {
599                format!(
600                    "producer-{}",
601                    PRODUCER_CLIENT_ID_SEQUENCE.fetch_add(1, Ordering::Relaxed)
602                )
603            },
604            |transactional_id| format!("producer-{transactional_id}"),
605        )
606    })
607}
608
609fn disabled_idempotence_identity() -> (i64, i16) {
610    (-1, -1)
611}
612
613fn initial_txn_pid_epoch() -> (i64, i16) {
614    (-1, -1)
615}
616
617fn producer_identity_from_init(init: &InitProducerIdResponse) -> Result<(i64, i16), ProducerError> {
618    if init.error_code != 0 {
619        return Err(ProducerError::Server(init.error_code));
620    }
621    Ok((init.producer_id, init.producer_epoch))
622}
623
624/// Send one `InitProducerId` request.
625///
626/// A request that carries a two-phase-commit field needs v6, which holds those
627/// fields. Every other request negotiates a released version, which is v5 at
628/// most. See [`StableInitProducerId`].
629pub async fn send_init_producer_id(
630    client: &Client,
631    request: &InitProducerIdRequest,
632) -> Result<InitProducerIdResponse, ClientError> {
633    if request.enable2_pc || request.keep_prepared_txn {
634        client
635            .send_at_least(
636                TwoPhaseCommitInitProducerId(request.clone()),
637                INIT_PRODUCER_ID_2PC_MIN_VERSION,
638            )
639            .await
640    } else {
641        client.send(StableInitProducerId(request.clone())).await
642    }
643}
644
645/// Send `InitProducerId`, and retry every code that Kafka's
646/// `InitProducerIdHandler` retries, and a transient `Disconnected` transport
647/// error, with capped exponential backoff until the deadline elapses.
648///
649/// Kafka's handler finds the coordinator again for `COORDINATOR_NOT_AVAILABLE`
650/// and `NOT_COORDINATOR`, and after a disconnect. This producer has no
651/// transaction coordinator for the idempotent `InitProducerId`, which goes to
652/// the bootstrap connection, so it reconnects that connection instead.
653/// [`Producer::init_transactions`] has a coordinator, and it runs its own loop
654/// that finds the coordinator again.
655///
656/// On the deadline it returns the last response, so the caller's
657/// `error_code != 0` handling runs. If the final attempt disconnected, it
658/// surfaces the transport error instead.
659#[tracing::instrument(level = "info", skip_all, err(level = "info"))]
660pub async fn init_producer_id_with_retry(
661    client: &Client,
662    request: InitProducerIdRequest,
663    retry_timeout: Time,
664    initial_backoff: Time,
665    max_backoff: Time,
666) -> Result<InitProducerIdResponse, ProducerError> {
667    let deadline = Instant::now()
668        .checked_add(retry_timeout.to_std())
669        .ok_or_else(|| {
670            ProducerError::InvalidConfig("producer-ID retry timeout is too large".to_owned())
671        })?;
672    let mut backoff = initial_backoff.to_std();
673    loop {
674        let remaining = deadline.saturating_duration_since(Instant::now());
675        let response = time::timeout(remaining, send_init_producer_id(client, &request))
676            .await
677            .map_err(|_| ProducerError::Client(ClientError::Timeout(retry_timeout)))?;
678        let (attempt, last_outcome) = match response {
679            Ok(resp) => (CoordinatorAttempt::Answered(resp.error_code), Ok(resp)),
680            Err(error @ ClientError::Disconnected) => (CoordinatorAttempt::Lost, Err(error)),
681            Err(e) => return Err(ProducerError::Client(e)),
682        };
683        let TxnRequestDecision::Retry { rediscover } = txn_retry::decide_init_producer_id(attempt)
684        else {
685            return last_outcome.map_err(ProducerError::Client);
686        };
687        let remaining = deadline.saturating_duration_since(Instant::now());
688        if remaining.is_zero() {
689            return last_outcome.map_err(ProducerError::Client);
690        }
691        if rediscover {
692            client.reconnect_bootstrap().await;
693        }
694        let sleep_for = backoff.min(remaining);
695        time::sleep(sleep_for).await;
696        if Instant::now() >= deadline {
697            return last_outcome.map_err(ProducerError::Client);
698        }
699        backoff = next_backoff(backoff, max_backoff.to_std());
700    }
701}
702
703/// The transaction timeout, or its default. Two-phase commit (KIP-939) does
704/// not allow `transaction.timeout.ms`, and it needs a transactional id.
705fn resolve_transaction_timeout(
706    enabled: bool,
707    transaction_timeout: Option<Duration>,
708    transactional_id: Option<&str>,
709) -> Result<Duration, ProducerError> {
710    if enabled && transaction_timeout.is_some() {
711        return Err(ProducerError::InvalidConfig(
712            "transaction_timeout cannot be set when transaction_two_phase_commit_enable=true"
713                .to_owned(),
714        ));
715    }
716    if enabled && transactional_id.is_none() {
717        return Err(ProducerError::InvalidConfig(
718            "transaction_two_phase_commit_enable=true requires transactional_id".to_owned(),
719        ));
720    }
721    Ok(transaction_timeout.unwrap_or(DEFAULT_PRODUCER_TRANSACTION_TIMEOUT))
722}
723
724/// Kafka's producer `send.buffer.bytes` default (128 KiB).
725pub const DEFAULT_PRODUCER_SEND_BUFFER: ByteSize = krabka_units::kibibytes(128);
726
727/// Kafka's producer `receive.buffer.bytes` default (32 KiB), half the
728/// consumer and admin default.
729pub const DEFAULT_PRODUCER_RECEIVE_BUFFER: ByteSize = krabka_units::kibibytes(32);
730
731/// Kafka's `ProducerMetadata` names the topics that the producer sends to,
732/// and lets the broker create a missing one.
733const PRODUCER_METADATA_SCOPE: krabka_client_core::MetadataScope =
734    krabka_client_core::MetadataScope::Topics {
735        allow_auto_topic_creation: true,
736    };
737
738/// The raw `Producer::start` builder arguments that configuration validation
739/// and derivation consume, before any connection is opened.
740struct RawProducerConfig<'a> {
741    client_id: Option<String>,
742    compression: Compression,
743    compression_gzip_level: i32,
744    compression_lz4_level: i32,
745    compression_zstd_level: i32,
746    enable_idempotence: Option<bool>,
747    acks: Acks,
748    linger: Duration,
749    batch_size: usize,
750    max_in_flight_per_connection: usize,
751    dns_timeout: Time,
752    dispatch_queue_capacity: usize,
753    frame_max: ByteSize,
754    request_timeout: Duration,
755    flush_timeout: Duration,
756    retries: i32,
757    retry_backoff: Duration,
758    retry_backoff_max: Duration,
759    delivery_timeout: Option<Duration>,
760    init_retry_timeout: Duration,
761    metadata_max_idle: Duration,
762    buffer_memory: usize,
763    max_request_size: usize,
764    metadata_recovery_rebootstrap_trigger: Time,
765    transactional_id: Option<&'a str>,
766    transaction_timeout: Option<Duration>,
767    transaction_two_phase_commit_enable: bool,
768}
769
770/// Config resolved and validated from a [`RawProducerConfig`], with Kafka's
771/// `ProducerConfig` rules (idempotence, throughput policy, retry policy,
772/// derived timeouts) applied.
773struct ResolvedProducerConfig {
774    client_id: String,
775    enable_idempotence: bool,
776    dns_timeout: ClientDnsTimeout,
777    dispatch_queue_capacity: ConnectionDispatchQueueCapacity,
778    frame_max: ClientFrameMax,
779    metadata_recovery_rebootstrap_trigger: krabka_client_core::MetadataRecoveryRebootstrapTrigger,
780    compression: Compression,
781    compression_levels: CompressionLevels,
782    linger: Time,
783    batch_size: usize,
784    max_in_flight_per_connection: usize,
785    buffer_pool: BufferPool,
786    max_request_size: usize,
787    retry_policy: ProducerRetryPolicy,
788    request_timeout: Time,
789    retries: i32,
790    retry_backoff: sender::RetryBackoff,
791    first_backoff: Time,
792    delivery_timeout: Time,
793    flush_timeout: ProducerFlushTimeout,
794}
795
796/// Validate a [`RawProducerConfig`] and derive the settings that shape the
797/// inner client, the sender's throughput and retries, and the transaction
798/// timeout, applying Kafka's `ProducerConfig` rules.
799///
800/// # Errors
801/// Returns an error when any raw builder argument violates those rules.
802fn resolve_producer_config(
803    raw: RawProducerConfig<'_>,
804) -> Result<ResolvedProducerConfig, ProducerError> {
805    let enable_idempotence = resolve_idempotence(
806        raw.enable_idempotence,
807        raw.acks,
808        raw.retries,
809        raw.max_in_flight_per_connection,
810        raw.transactional_id,
811    )?;
812    let client_id = resolve_client_id(raw.client_id, raw.transactional_id);
813    validate_metadata_max_idle(raw.metadata_max_idle)?;
814    let transaction_timeout = resolve_transaction_timeout(
815        raw.transaction_two_phase_commit_enable,
816        raw.transaction_timeout,
817        raw.transactional_id,
818    )?;
819    let dns_timeout =
820        ClientDnsTimeout::new(raw.dns_timeout).map_err(ProducerError::InvalidConfig)?;
821    let dispatch_queue_capacity = ConnectionDispatchQueueCapacity::new(raw.dispatch_queue_capacity)
822        .map_err(ProducerError::InvalidConfig)?;
823    let frame_max =
824        ClientFrameMax::try_from(raw.frame_max).map_err(ProducerError::InvalidConfig)?;
825    let metadata_recovery_rebootstrap_trigger =
826        krabka_client_core::MetadataRecoveryRebootstrapTrigger::new(
827            raw.metadata_recovery_rebootstrap_trigger,
828        )
829        .map_err(ProducerError::InvalidConfig)?;
830    let throughput_policy = ProducerThroughputPolicy::new(
831        raw.compression,
832        raw.linger,
833        raw.batch_size,
834        raw.max_in_flight_per_connection,
835    )
836    .map_err(ProducerError::InvalidConfig)?;
837    let compression = throughput_policy.compression();
838    let compression_levels = CompressionLevels::new(
839        raw.compression_gzip_level,
840        raw.compression_lz4_level,
841        raw.compression_zstd_level,
842    )
843    .map_err(ProducerError::InvalidConfig)?;
844    let linger = throughput_policy.linger().as_time();
845    let batch_size = throughput_policy.batch_bytes();
846    let max_in_flight_per_connection = throughput_policy.max_in_flight();
847    let buffer_pool =
848        BufferPool::new(raw.buffer_memory, batch_size).map_err(ProducerError::InvalidConfig)?;
849    let max_request_size = validated_max_request_size(raw.max_request_size)?;
850
851    let retry_policy = ProducerRetryPolicy::new(
852        raw.request_timeout,
853        raw.retries,
854        raw.retry_backoff,
855        raw.retry_backoff_max,
856        raw.init_retry_timeout,
857        transaction_timeout,
858    )
859    .map_err(ProducerError::InvalidConfig)?;
860    let delivery_timeout = resolve_delivery_timeout(
861        raw.delivery_timeout,
862        throughput_policy.linger(),
863        retry_policy.request_timeout(),
864    )
865    .map_err(ProducerError::InvalidConfig)?;
866    // The validated retry policy derives `Eq`, so it holds `Duration`s; the
867    // domain past this point holds quantities.
868    let request_timeout = retry_policy.request_timeout().as_time();
869    let retries = retry_policy.retries();
870    let retry_backoff = sender::RetryBackoff::new(
871        retry_policy.retry_backoff(),
872        retry_policy.retry_backoff_max(),
873    );
874    let first_backoff = retry_policy.first_backoff();
875    let flush_timeout =
876        ProducerFlushTimeout::new(raw.flush_timeout).map_err(ProducerError::InvalidConfig)?;
877
878    Ok(ResolvedProducerConfig {
879        client_id,
880        enable_idempotence,
881        dns_timeout,
882        dispatch_queue_capacity,
883        frame_max,
884        metadata_recovery_rebootstrap_trigger,
885        compression,
886        compression_levels,
887        linger,
888        batch_size,
889        max_in_flight_per_connection,
890        buffer_pool,
891        max_request_size,
892        retry_policy,
893        request_timeout,
894        retries,
895        retry_backoff,
896        first_backoff,
897        delivery_timeout: delivery_timeout.as_time(),
898        flush_timeout,
899    })
900}
901
902/// Inputs to [`spawn_producer_runtime`]: the settings needed to build the
903/// `Arc`-wrapped state that `Producer` shares with its sender task, and to
904/// spawn the background tasks that drive it.
905#[derive(Clone, Copy)]
906struct RuntimeInputs<'a> {
907    retry_policy: ProducerRetryPolicy,
908    metadata_max_age: Duration,
909    metadata_max_idle: Duration,
910    batch_size: usize,
911    partitioner_ignore_keys: bool,
912    partitioner_adaptive_partitioning_enable: bool,
913    partitioner_availability_timeout: Duration,
914    partitioner_rack_aware: bool,
915    producer_id: i64,
916    producer_epoch: i16,
917    acks: Acks,
918    compression: Compression,
919    compression_levels: CompressionLevels,
920    linger: Time,
921    retries: i32,
922    retry_backoff: sender::RetryBackoff,
923    delivery_timeout: Time,
924    max_in_flight_per_connection: usize,
925    transactional_id: Option<&'a str>,
926}
927
928/// The `Arc`-wrapped state that `Producer` shares with its sender task, and
929/// the handle of the sender task itself.
930struct ProducerRuntime {
931    wake_tx: mpsc::Sender<sender::DrainIntent>,
932    shutdown: CancellationToken,
933    state: Arc<AtomicU8>,
934    metadata_cache: Arc<producer::MetadataCache>,
935    metadata_refresh: Arc<metadata_wait::MetadataRefresh>,
936    partition_leaders: Arc<DashMap<(String, i32), i32>>,
937    accumulators: AccumulatorMap,
938    next_seq: Arc<DashMap<(String, i32), i32>>,
939    partitioner: Arc<BuiltInPartitioner>,
940    flush_notify: Arc<Notify>,
941    in_flight: Arc<AtomicUsize>,
942    producer_id: Arc<AtomicI64>,
943    producer_epoch: Arc<AtomicI16>,
944    txn_state: Arc<Mutex<TxnState>>,
945    txn_recovery_required: Arc<AtomicBool>,
946    txn_recovery_generation: Arc<AtomicU64>,
947    txn_guard_generation: Arc<AtomicU64>,
948    txn_pid_epoch: Arc<Mutex<(i64, i16)>>,
949    prepared_transaction_state: Arc<Mutex<Option<PreparedTransactionState>>>,
950    txn_error: Arc<TxnErrorSlot>,
951    transaction_v2: Arc<AtomicBool>,
952    sender_handle: JoinHandle<()>,
953}
954
955/// Build the shared state of a [`ProducerRuntime`] and spawn the metadata-age
956/// and sender background tasks that drive it.
957fn spawn_producer_runtime(client: &Client, inputs: RuntimeInputs<'_>) -> ProducerRuntime {
958    let RuntimeInputs {
959        retry_policy,
960        metadata_max_age,
961        metadata_max_idle,
962        batch_size,
963        partitioner_ignore_keys,
964        partitioner_adaptive_partitioning_enable,
965        partitioner_availability_timeout,
966        partitioner_rack_aware,
967        producer_id,
968        producer_epoch,
969        acks,
970        compression,
971        compression_levels,
972        linger,
973        retries,
974        retry_backoff,
975        delivery_timeout,
976        max_in_flight_per_connection,
977        transactional_id,
978    } = inputs;
979
980    let (wake_tx, wake_rx) = mpsc::channel(SENDER_WAKE_CHANNEL_CAPACITY);
981    let shutdown = CancellationToken::new();
982    let state = Arc::new(AtomicU8::new(0));
983    let metadata_cache = Arc::new(producer::MetadataCache::default());
984    let partition_leaders = Arc::new(DashMap::new());
985    let accumulators = Arc::new(DashMap::new());
986    let next_seq = Arc::new(DashMap::new());
987    let partitioner = Arc::new(BuiltInPartitioner::new(PartitionerConfig {
988        sticky_batch_size: batch_size,
989        ignore_keys: partitioner_ignore_keys,
990        adaptive_partitioning: partitioner_adaptive_partitioning_enable,
991        availability_timeout: partitioner_availability_timeout,
992        rack_aware: partitioner_rack_aware,
993    }));
994    let flush_notify = Arc::new(Notify::new());
995    let in_flight = Arc::new(AtomicUsize::new(0));
996    let producer_id = Arc::new(AtomicI64::new(producer_id));
997    let producer_epoch = Arc::new(AtomicI16::new(producer_epoch));
998
999    let txn_state = Arc::new(Mutex::new(TxnState::Uninitialized));
1000    let txn_recovery_required = Arc::new(AtomicBool::new(false));
1001    let txn_recovery_generation = Arc::new(AtomicU64::new(0));
1002    let txn_guard_generation = Arc::new(AtomicU64::new(0));
1003    let txn_pid_epoch = Arc::new(Mutex::new(initial_txn_pid_epoch()));
1004    let prepared_transaction_state = Arc::new(Mutex::new(None));
1005    let txn_error = Arc::new(TxnErrorSlot::default());
1006    let transaction_v2 = Arc::new(AtomicBool::new(false));
1007
1008    let metadata_refresh = Arc::new(metadata_wait::MetadataRefresh::default());
1009    MetadataAge::new(client, &retry_policy)
1010        .with_ages(metadata_max_age, metadata_max_idle)
1011        .with_caches(&metadata_cache, &partition_leaders, &metadata_refresh)
1012        .with_queues((&accumulators, &in_flight))
1013        .spawn(shutdown.clone());
1014    let sender_config = sender::SenderConfig {
1015        transport: Box::new(ClientTransport::new(
1016            client.clone(),
1017            Arc::clone(&transaction_v2),
1018        )),
1019        producer_id: Arc::clone(&producer_id),
1020        producer_epoch: Arc::clone(&producer_epoch),
1021        acks,
1022        compression,
1023        compression_level: compression_levels.level(compression),
1024        linger,
1025        request_timeout_ms: retry_policy.request_timeout_ms(),
1026        retries,
1027        retry_backoff,
1028        delivery_timeout,
1029        max_in_flight: max_in_flight_per_connection,
1030        metadata_cache: Arc::clone(&metadata_cache),
1031        partition_leaders: Arc::clone(&partition_leaders),
1032        partitioner: Arc::clone(&partitioner),
1033        accumulators: Arc::clone(&accumulators),
1034        next_seq: Arc::clone(&next_seq),
1035        state: Arc::clone(&state),
1036        flush_notify: Arc::clone(&flush_notify),
1037        in_flight: Arc::clone(&in_flight),
1038        shutdown: shutdown.clone(),
1039        transactional_id: transactional_id.map(str::to_owned),
1040        txn_state: Arc::clone(&txn_state),
1041        txn_pid_epoch: Arc::clone(&txn_pid_epoch),
1042        txn_recovery_required: Arc::clone(&txn_recovery_required),
1043        txn_recovery_generation: Arc::clone(&txn_recovery_generation),
1044        txn_error: Arc::clone(&txn_error),
1045    };
1046    let sender_handle = tokio::spawn(sender::run(sender_config, wake_rx));
1047
1048    ProducerRuntime {
1049        wake_tx,
1050        shutdown,
1051        state,
1052        metadata_cache,
1053        metadata_refresh,
1054        partition_leaders,
1055        accumulators,
1056        next_seq,
1057        partitioner,
1058        flush_notify,
1059        in_flight,
1060        producer_id,
1061        producer_epoch,
1062        txn_state,
1063        txn_recovery_required,
1064        txn_recovery_generation,
1065        txn_guard_generation,
1066        txn_pid_epoch,
1067        prepared_transaction_state,
1068        txn_error,
1069        transaction_v2,
1070        sender_handle,
1071    }
1072}
1073
1074async fn initial_producer_identity(
1075    client: &Client,
1076    enable_idempotence: bool,
1077    retry_policy: ProducerRetryPolicy,
1078    first_backoff: Time,
1079) -> Result<(i64, i16), ProducerError> {
1080    if !enable_idempotence {
1081        return Ok(disabled_idempotence_identity());
1082    }
1083    let init = init_producer_id_with_retry(
1084        client,
1085        build_init_producer_id_request(),
1086        retry_policy.init_retry_timeout().as_time(),
1087        first_backoff,
1088        retry_policy.retry_backoff_max().as_time(),
1089    )
1090    .await?;
1091    producer_identity_from_init(&init)
1092}
1093
1094fn producer_telemetry(
1095    enabled: bool,
1096    transactional_id: Option<&str>,
1097) -> Option<telemetry::ClientTelemetryConfig> {
1098    enabled.then(|| telemetry::ClientTelemetryConfig::producer(transactional_id))
1099}
1100
1101#[bon::bon]
1102impl Producer {
1103    /// Build a [`Producer`] pointed at the given bootstrap address.
1104    ///
1105    /// The defaults are Kafka's: `acks=All`, a linger of 5 ms, and
1106    /// idempotence on. The builder applies the rules of Kafka's
1107    /// `ProducerConfig`. When `enable_idempotence` is not set, `retries=0` or
1108    /// an `acks` other than `All` turns idempotence off. When it is set to
1109    /// `true`, those settings fail with [`ProducerError::InvalidConfig`]. An
1110    /// idempotent producer accepts at most 5 in-flight requests per
1111    /// connection, and a `transactional_id` requires idempotence.
1112    ///
1113    /// `enable_metrics_push` (Kafka's `enable.metrics.push`, default `true`)
1114    /// pushes the client metrics that a broker's client metrics subscription
1115    /// names (KIP-714), with a terminating push on close. When it is `false`,
1116    /// the producer sends neither `GetTelemetrySubscriptions` nor
1117    /// `PushTelemetry`.
1118    ///
1119    /// `send` fails a record whose serialized size is larger than
1120    /// `max_request_size` (default 1 MiB, Kafka's `max.request.size`) or
1121    /// larger than `buffer_memory` with [`ProducerError::RecordTooLarge`], and
1122    /// sends no request for it.
1123    ///
1124    /// `compression_gzip_level`, `compression_lz4_level` and
1125    /// `compression_zstd_level` take Kafka's defaults (-1, 9 and 3) and ranges
1126    /// (KIP-390), and `build` fails with [`ProducerError::InvalidConfig`] for a
1127    /// level out of range. Each batch of a gzip, lz4 or zstd producer is
1128    /// compressed at the level of its codec. The lz4 codec has no high
1129    /// compression mode, so every lz4 level gives the same output
1130    /// (krabka-io/krabka-protocol#27).
1131    ///
1132    /// When `client_id` is not set, the client id is
1133    /// `producer-<transactional_id>`, or `producer-<n>` with a process-wide
1134    /// sequence number.
1135    #[builder(start_fn = builder, finish_fn = build)]
1136    // bon builder; each arg is an independent knob
1137    #[tracing::instrument(
1138        level = "info",
1139        skip_all,
1140        fields(
1141            bootstrap = %bootstrap,
1142            client_id = client_id.as_deref(),
1143            acks = ?acks,
1144            enable_idempotence = ?enable_idempotence,
1145            transactional_id = transactional_id.as_deref(),
1146        ),
1147        err(level = "info"),
1148    )]
1149    /// # Errors
1150    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
1151    pub async fn start(
1152        #[builder(into)] bootstrap: String,
1153        #[builder(into)] client_id: Option<String>,
1154        #[builder(default = DEFAULT_PRODUCER_COMPRESSION)] compression: Compression,
1155        #[builder(default = DEFAULT_PRODUCER_COMPRESSION_GZIP_LEVEL)] compression_gzip_level: i32,
1156        #[builder(default = DEFAULT_PRODUCER_COMPRESSION_LZ4_LEVEL)] compression_lz4_level: i32,
1157        #[builder(default = DEFAULT_PRODUCER_COMPRESSION_ZSTD_LEVEL)] compression_zstd_level: i32,
1158        enable_idempotence: Option<bool>,
1159        #[builder(default = DEFAULT_PRODUCER_ACKS)] acks: Acks,
1160        #[builder(default = DEFAULT_PRODUCER_LINGER)] linger: Duration,
1161        #[builder(default = DEFAULT_PRODUCER_BATCH_BYTES)] batch_size: usize,
1162        #[builder(default = DEFAULT_PRODUCER_PARTITIONER_IGNORE_KEYS)]
1163        partitioner_ignore_keys: bool,
1164        #[builder(default = DEFAULT_PRODUCER_PARTITIONER_ADAPTIVE_PARTITIONING_ENABLE)]
1165        partitioner_adaptive_partitioning_enable: bool,
1166        #[builder(default = DEFAULT_PRODUCER_PARTITIONER_AVAILABILITY_TIMEOUT)]
1167        partitioner_availability_timeout: Duration,
1168        /// Kafka's `partitioner.class`. Set, `send` calls it instead of the
1169        /// built-in partitioner for a record with no explicit partition, and
1170        /// adaptive partitioning turns off. See
1171        /// [`Partitioner`](crate::partitioner::Partitioner).
1172        partitioner: Option<Arc<dyn Partitioner>>,
1173        #[builder(into)] client_rack: Option<String>,
1174        #[builder(default = DEFAULT_PRODUCER_PARTITIONER_RACK_AWARE)] partitioner_rack_aware: bool,
1175        #[builder(default = DEFAULT_CLIENT_DNS_TIMEOUT)] dns_timeout: Time,
1176        /// DNS address selection policy for all producer clients.
1177        #[builder(default)]
1178        client_dns_lookup: krabka_client_core::ClientDnsLookup,
1179        /// Initial delay before reconnecting to an unavailable broker.
1180        #[builder(default = krabka_client_core::DEFAULT_RECONNECT_BACKOFF)]
1181        reconnect_backoff: Time,
1182        /// Maximum reconnect delay.
1183        #[builder(default = krabka_client_core::DEFAULT_RECONNECT_BACKOFF_MAX)]
1184        reconnect_backoff_max: Time,
1185        /// Maximum interval without requests before a connection is closed.
1186        #[builder(default = krabka_client_core::DEFAULT_CONNECTIONS_MAX_IDLE)]
1187        connections_max_idle: Time,
1188        /// Initial deadline for opening and negotiating a connection.
1189        #[builder(default = krabka_client_core::DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT)]
1190        socket_connection_setup_timeout: Time,
1191        /// Maximum deadline after repeated connection failures.
1192        #[builder(default = krabka_client_core::DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT_MAX)]
1193        socket_connection_setup_timeout_max: Time,
1194        #[builder(default = DEFAULT_CONNECTION_DISPATCH_QUEUE_CAPACITY)]
1195        dispatch_queue_capacity: usize,
1196        #[builder(default = DEFAULT_CLIENT_FRAME_MAX)] frame_max: ByteSize,
1197        /// Kafka's `send.buffer.bytes`: the `SO_SNDBUF` of each broker
1198        /// socket. `None` keeps the operating system default, as Kafka's
1199        /// `-1` does.
1200        #[builder(required, default = Some(DEFAULT_PRODUCER_SEND_BUFFER))]
1201        send_buffer: Option<ByteSize>,
1202        /// Kafka's `receive.buffer.bytes`: the `SO_RCVBUF` of each broker
1203        /// socket. `None` keeps the operating system default, as Kafka's
1204        /// `-1` does.
1205        #[builder(required, default = Some(DEFAULT_PRODUCER_RECEIVE_BUFFER))]
1206        receive_buffer: Option<ByteSize>,
1207        #[builder(default = DEFAULT_PRODUCER_REQUEST_TIMEOUT)] request_timeout: Duration,
1208        #[builder(default = DEFAULT_PRODUCER_FLUSH_TIMEOUT)] flush_timeout: Duration,
1209        #[builder(default = DEFAULT_PRODUCER_RETRIES)] retries: i32,
1210        #[builder(default = DEFAULT_PRODUCER_RETRY_BACKOFF)] retry_backoff: Duration,
1211        #[builder(default = DEFAULT_PRODUCER_RETRY_BACKOFF_MAX)] retry_backoff_max: Duration,
1212        delivery_timeout: Option<Duration>,
1213        #[builder(default = DEFAULT_PRODUCER_INIT_RETRY_TIMEOUT)] init_retry_timeout: Duration,
1214        #[builder(default = DEFAULT_PRODUCER_MAX_IN_FLIGHT)] max_in_flight_per_connection: usize,
1215        #[builder(default = DEFAULT_PRODUCER_MAX_BLOCK)] max_block: Duration,
1216        #[builder(default = DEFAULT_PRODUCER_METADATA_MAX_AGE)] metadata_max_age: Duration,
1217        #[builder(default = DEFAULT_PRODUCER_METADATA_MAX_IDLE)] metadata_max_idle: Duration,
1218        #[builder(default = DEFAULT_PRODUCER_BUFFER_MEMORY)] buffer_memory: usize,
1219        #[builder(default = DEFAULT_PRODUCER_MAX_REQUEST_SIZE)] max_request_size: usize,
1220        #[builder(default)]
1221        metadata_recovery_strategy: krabka_client_core::MetadataRecoveryStrategy,
1222        #[builder(default = krabka_client_core::DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER)]
1223        metadata_recovery_rebootstrap_trigger: Time,
1224        #[builder(into)] transactional_id: Option<String>,
1225        transaction_timeout: Option<Duration>,
1226        #[builder(default)] transaction_two_phase_commit_enable: bool,
1227        security: Option<ClientSecurity>,
1228        #[builder(default = true)] enable_metrics_push: bool,
1229    ) -> Result<Self, ProducerError> {
1230        let telemetry = producer_telemetry(enable_metrics_push, transactional_id.as_deref());
1231        let ResolvedProducerConfig {
1232            client_id,
1233            enable_idempotence,
1234            dns_timeout,
1235            dispatch_queue_capacity,
1236            frame_max,
1237            metadata_recovery_rebootstrap_trigger,
1238            compression,
1239            compression_levels,
1240            linger,
1241            batch_size,
1242            max_in_flight_per_connection,
1243            buffer_pool,
1244            max_request_size,
1245            retry_policy,
1246            request_timeout,
1247            retries,
1248            retry_backoff,
1249            first_backoff,
1250            delivery_timeout,
1251            flush_timeout,
1252        } = resolve_producer_config(RawProducerConfig {
1253            client_id,
1254            compression,
1255            compression_gzip_level,
1256            compression_lz4_level,
1257            compression_zstd_level,
1258            enable_idempotence,
1259            acks,
1260            linger,
1261            batch_size,
1262            max_in_flight_per_connection,
1263            dns_timeout,
1264            dispatch_queue_capacity,
1265            frame_max,
1266            request_timeout,
1267            flush_timeout,
1268            retries,
1269            retry_backoff,
1270            retry_backoff_max,
1271            delivery_timeout,
1272            init_retry_timeout,
1273            metadata_max_idle,
1274            buffer_memory,
1275            max_request_size,
1276            metadata_recovery_rebootstrap_trigger,
1277            transactional_id: transactional_id.as_deref(),
1278            transaction_timeout,
1279            transaction_two_phase_commit_enable,
1280        })?;
1281
1282        // 1. Build inner client. `security` is cloned (not moved) so it can be
1283        //    retained on the `Producer` and reused for the secondary
1284        //    coordinator connections opened by the transactional path.
1285        let client = Client::builder()
1286            .bootstrap(bootstrap)
1287            .client_id(client_id.clone())
1288            .dns_timeout(dns_timeout.time())
1289            .client_dns_lookup(client_dns_lookup)
1290            .reconnect_backoff(reconnect_backoff)
1291            .reconnect_backoff_max(reconnect_backoff_max)
1292            .connections_max_idle(connections_max_idle)
1293            .socket_connection_setup_timeout(socket_connection_setup_timeout)
1294            .socket_connection_setup_timeout_max(socket_connection_setup_timeout_max)
1295            .dispatch_queue_capacity(dispatch_queue_capacity.get())
1296            .frame_max(frame_max.size())
1297            .request_timeout(request_timeout)
1298            .send_buffer(send_buffer)
1299            .receive_buffer(receive_buffer)
1300            .metadata_recovery_strategy(metadata_recovery_strategy)
1301            .metadata_recovery_rebootstrap_trigger(metadata_recovery_rebootstrap_trigger.time())
1302            .maybe_security(security.clone())
1303            .metadata_scope(PRODUCER_METADATA_SCOPE)
1304            .maybe_telemetry(telemetry)
1305            .build()
1306            .await?;
1307
1308        // 2. InitProducerId if idempotence on.
1309        //
1310        // Idempotent-only producers (no transactional_id) allocate a producer
1311        // id from *any* broker — no FindCoordinator routing to a transaction
1312        // coordinator is required. But at cluster startup the broker can still
1313        // transiently return COORDINATOR_LOAD_IN_PROGRESS (14) /
1314        // COORDINATOR_NOT_AVAILABLE (15) / NOT_COORDINATOR (16) while its
1315        // internal state loads, and a conformant client retries these with
1316        // backoff rather than surfacing the first error. `init_producer_id`
1317        // does that retry and returns the final response.
1318        let (producer_id, producer_epoch) =
1319            initial_producer_identity(&client, enable_idempotence, retry_policy, first_backoff)
1320                .await?;
1321
1322        // 3. Spawn the sender. The runtime binds the built-in partitioner to
1323        //    `partitioner`, so the `partitioner.class` plugin moves aside first.
1324        let partitioner_plugin = partitioner;
1325        let ProducerRuntime {
1326            wake_tx,
1327            shutdown,
1328            state,
1329            metadata_cache,
1330            metadata_refresh,
1331            partition_leaders,
1332            accumulators,
1333            next_seq,
1334            partitioner,
1335            flush_notify,
1336            in_flight,
1337            producer_id: shared_producer_id,
1338            producer_epoch,
1339            txn_state,
1340            txn_recovery_required,
1341            txn_recovery_generation,
1342            txn_guard_generation,
1343            txn_pid_epoch,
1344            prepared_transaction_state,
1345            txn_error,
1346            transaction_v2,
1347            sender_handle,
1348        } = spawn_producer_runtime(
1349            &client,
1350            RuntimeInputs {
1351                retry_policy,
1352                metadata_max_age,
1353                metadata_max_idle,
1354                batch_size,
1355                partitioner_ignore_keys,
1356                // Kafka's constructor: `enableAdaptivePartitioning =
1357                // partitionerPlugin.get() == null && ...`.
1358                partitioner_adaptive_partitioning_enable: partitioner_adaptive_partitioning_enable
1359                    && partitioner_plugin.is_none(),
1360                partitioner_availability_timeout,
1361                partitioner_rack_aware,
1362                producer_id,
1363                producer_epoch,
1364                acks,
1365                compression,
1366                compression_levels,
1367                linger,
1368                retries,
1369                retry_backoff,
1370                delivery_timeout,
1371                max_in_flight_per_connection,
1372                transactional_id: transactional_id.as_deref(),
1373            },
1374        );
1375
1376        Ok(Self {
1377            client,
1378            client_id,
1379            security,
1380            dns_timeout: dns_timeout.time(),
1381            metadata_recovery_strategy,
1382            metadata_recovery_rebootstrap_trigger,
1383            client_dns_lookup,
1384            reconnect_backoff,
1385            reconnect_backoff_max,
1386            connections_max_idle,
1387            socket_connection_setup_timeout,
1388            socket_connection_setup_timeout_max,
1389            dispatch_queue_capacity,
1390            frame_max,
1391            socket_buffers: SocketBuffers {
1392                send: send_buffer,
1393                receive: receive_buffer,
1394            },
1395            identity: ProducerIdentity {
1396                id: shared_producer_id,
1397                epoch: producer_epoch,
1398            },
1399            acks,
1400            compression,
1401            compression_levels,
1402            batch_size,
1403            linger,
1404            request_timeout,
1405            flush_timeout,
1406            max_block,
1407            buffer_pool,
1408            max_request_size,
1409            max_in_flight: max_in_flight_per_connection,
1410            metadata_cache,
1411            metadata_refresh,
1412            partition_leaders,
1413            broker_racks: Arc::new(DashMap::new()),
1414            accumulators,
1415            next_seq,
1416            partitioner,
1417            custom_partitioner: partitioner_plugin,
1418            client_rack,
1419            state,
1420            wake_tx,
1421            flush_notify,
1422            in_flight,
1423            sender_shutdown: shutdown,
1424            sender_handle: Some(sender_handle),
1425            transactional_id,
1426            transaction_timeout_ms: retry_policy.transaction_timeout_ms(),
1427            two_phase_commit_enabled: transaction_two_phase_commit_enable,
1428            init_retry_timeout: retry_policy.init_retry_timeout().as_time(),
1429            init_retry_backoff: first_backoff,
1430            retry_backoff_max: retry_policy.retry_backoff_max().as_time(),
1431            txn_state,
1432            txn_recovery_required,
1433            txn_recovery_generation,
1434            txn_guard_generation,
1435            txn_coord_client: Mutex::new(None),
1436            txn_pid_epoch,
1437            txn_error,
1438            transaction_v2,
1439            prepared_transaction_state,
1440        })
1441    }
1442}
1443
1444#[cfg(test)]
1445mod security_arg_tests {
1446    use std::sync::{
1447        Arc, Mutex as StdMutex,
1448        atomic::{AtomicUsize, Ordering},
1449    };
1450
1451    use bytes::BytesMut;
1452    use krabka_client_core::{
1453        MockBroker, mock_test_support::recorded_levels, security::SaslCredentials,
1454    };
1455    use krabka_protocol::{
1456        Decode,
1457        owned::{
1458            api_versions_request,
1459            api_versions_response::{ApiVersion, ApiVersionsResponse},
1460            init_producer_id_response,
1461        },
1462    };
1463    use krabka_security::ListenerProtocol;
1464    use krabka_units::{micros, millis};
1465
1466    use super::*;
1467
1468    fn encode_v0(response: &impl Encode) -> Vec<u8> {
1469        let mut bytes = BytesMut::new();
1470        response.encode(&mut bytes, 0).expect("encode response");
1471        bytes.to_vec()
1472    }
1473
1474    /// Each `Default` builds the value that its validating constructor builds
1475    /// from the same `DEFAULT_PRODUCER_*` settings.
1476    #[test]
1477    fn the_defaults_pass_their_own_validation() {
1478        assert2::assert!(
1479            ProducerFlushTimeout::new(DEFAULT_PRODUCER_FLUSH_TIMEOUT)
1480                == Ok(ProducerFlushTimeout::default())
1481        );
1482        assert2::assert!(
1483            ProducerThroughputPolicy::new(
1484                DEFAULT_PRODUCER_COMPRESSION,
1485                DEFAULT_PRODUCER_LINGER,
1486                DEFAULT_PRODUCER_BATCH_BYTES,
1487                DEFAULT_PRODUCER_MAX_IN_FLIGHT,
1488            ) == Ok(ProducerThroughputPolicy::default())
1489        );
1490        assert2::assert!(
1491            ProducerRetryPolicy::new(
1492                DEFAULT_PRODUCER_REQUEST_TIMEOUT,
1493                DEFAULT_PRODUCER_RETRIES,
1494                DEFAULT_PRODUCER_RETRY_BACKOFF,
1495                DEFAULT_PRODUCER_RETRY_BACKOFF_MAX,
1496                DEFAULT_PRODUCER_INIT_RETRY_TIMEOUT,
1497                DEFAULT_PRODUCER_TRANSACTION_TIMEOUT,
1498            ) == Ok(ProducerRetryPolicy::default())
1499        );
1500    }
1501
1502    #[test]
1503    fn producer_retry_policy_defaults_and_distinct_values_are_exact() {
1504        let defaults = ProducerRetryPolicy::default();
1505        assert2::assert!(
1506            (
1507                defaults.request_timeout(),
1508                defaults.retries(),
1509                defaults.retry_backoff(),
1510                defaults.retry_backoff_max(),
1511                defaults.init_retry_timeout(),
1512                defaults.transaction_timeout(),
1513                defaults.request_timeout_ms(),
1514                defaults.transaction_timeout_ms(),
1515            ) == (
1516                Duration::from_secs(30),
1517                i32::MAX,
1518                Duration::from_millis(100),
1519                Duration::from_secs(1),
1520                Duration::from_secs(30),
1521                Duration::from_mins(1),
1522                30_000,
1523                60_000,
1524            )
1525        );
1526
1527        let policy = ProducerRetryPolicy::new(
1528            Duration::from_millis(11),
1529            12,
1530            Duration::from_millis(13),
1531            Duration::from_millis(14),
1532            Duration::from_millis(15),
1533            Duration::from_millis(17),
1534        )
1535        .expect("distinct policy");
1536        assert2::assert!(
1537            (
1538                policy.request_timeout(),
1539                policy.retries(),
1540                policy.retry_backoff(),
1541                policy.retry_backoff_max(),
1542                policy.init_retry_timeout(),
1543                policy.transaction_timeout(),
1544                policy.request_timeout_ms(),
1545                policy.transaction_timeout_ms(),
1546            ) == (
1547                Duration::from_millis(11),
1548                12,
1549                Duration::from_millis(13),
1550                Duration::from_millis(14),
1551                Duration::from_millis(15),
1552                Duration::from_millis(17),
1553                11,
1554                17,
1555            )
1556        );
1557    }
1558
1559    #[test]
1560    fn producer_flush_timeout_defaults_and_distinct_values_are_exact() {
1561        assert_eq!(
1562            (
1563                ProducerFlushTimeout::default().duration(),
1564                ProducerFlushTimeout::default().milliseconds(),
1565            ),
1566            (Duration::from_secs(50), 50_000),
1567        );
1568
1569        let timeout =
1570            ProducerFlushTimeout::new(Duration::from_millis(11)).expect("distinct timeout");
1571        assert_eq!(
1572            (timeout.duration(), timeout.milliseconds()),
1573            (Duration::from_millis(11), 11),
1574        );
1575    }
1576
1577    #[test]
1578    fn producer_flush_timeout_rejects_invalid_protocol_durations() {
1579        for timeout in [
1580            Duration::ZERO,
1581            Duration::from_nanos(1),
1582            Duration::from_millis(i32::MAX as u64 + 1),
1583        ] {
1584            assert!(
1585                ProducerFlushTimeout::new(timeout).is_err(),
1586                "{timeout:?} must be rejected"
1587            );
1588        }
1589    }
1590
1591    #[test]
1592    fn producer_throughput_policy_defaults_and_distinct_values_are_exact() {
1593        let defaults = ProducerThroughputPolicy::default();
1594        assert2::assert!(
1595            (
1596                defaults.compression(),
1597                defaults.linger(),
1598                defaults.batch_bytes(),
1599                defaults.max_in_flight(),
1600                defaults.linger_ms(),
1601            ) == (Compression::None, Duration::from_millis(5), 16_384, 5, 5)
1602        );
1603
1604        let policy =
1605            ProducerThroughputPolicy::new(Compression::Zstd, Duration::from_millis(11), 12, 13)
1606                .expect("distinct throughput policy");
1607        assert_eq!(
1608            (
1609                policy.compression(),
1610                policy.linger(),
1611                policy.batch_bytes(),
1612                policy.max_in_flight(),
1613                policy.linger_ms(),
1614            ),
1615            (Compression::Zstd, Duration::from_millis(11), 12, 13, 11,)
1616        );
1617    }
1618
1619    #[test]
1620    fn producer_throughput_policy_enforces_protocol_bounds() {
1621        assert!(
1622            ProducerThroughputPolicy::new(Compression::None, Duration::ZERO, 1, 1).is_ok(),
1623            "zero linger is valid"
1624        );
1625        for (error, field) in [
1626            (
1627                ProducerThroughputPolicy::new(
1628                    Compression::None,
1629                    Duration::from_millis(i32::MAX as u64 + 1),
1630                    1,
1631                    1,
1632                )
1633                .expect_err("overflow linger"),
1634                "producer linger",
1635            ),
1636            (
1637                ProducerThroughputPolicy::new(Compression::None, Duration::from_nanos(1), 1, 1)
1638                    .expect_err("fractional linger"),
1639                "producer linger",
1640            ),
1641            (
1642                ProducerThroughputPolicy::new(Compression::None, Duration::ZERO, 0, 1)
1643                    .expect_err("zero batch bytes"),
1644                "producer batch bytes",
1645            ),
1646            (
1647                ProducerThroughputPolicy::new(
1648                    Compression::None,
1649                    Duration::ZERO,
1650                    i32::MAX as usize + 1,
1651                    1,
1652                )
1653                .expect_err("overflow batch bytes"),
1654                "producer batch bytes",
1655            ),
1656            (
1657                ProducerThroughputPolicy::new(Compression::None, Duration::ZERO, 1, 0)
1658                    .expect_err("zero max in flight"),
1659                "producer max in flight",
1660            ),
1661        ] {
1662            assert!(error.contains(field), "{error:?} does not name {field:?}");
1663        }
1664    }
1665
1666    #[test]
1667    fn producer_retry_policy_rejects_invalid_bounds() {
1668        let one = Duration::from_millis(1);
1669        let zero = Duration::ZERO;
1670        let overflow = Duration::from_millis(i32::MAX as u64 + 1);
1671        let cases = [
1672            ("zero request", zero, 0, one, one, one, one),
1673            ("negative retries", one, -1, one, one, one, one),
1674            ("zero backoff", one, 0, zero, one, one, one),
1675            ("zero backoff maximum", one, 0, one, zero, one, one),
1676            ("zero init", one, 0, one, one, zero, one),
1677            ("zero transaction", one, 0, one, one, one, zero),
1678            ("request protocol overflow", overflow, 0, one, one, one, one),
1679            (
1680                "transaction protocol overflow",
1681                one,
1682                0,
1683                one,
1684                one,
1685                one,
1686                overflow,
1687            ),
1688            (
1689                "backoff maximum overflow",
1690                one,
1691                0,
1692                one,
1693                Duration::MAX,
1694                one,
1695                one,
1696            ),
1697        ];
1698        let rejected = cases
1699            .iter()
1700            .map(
1701                |&(name, request, retries, backoff, max, init, transaction)| {
1702                    (
1703                        name,
1704                        ProducerRetryPolicy::new(request, retries, backoff, max, init, transaction)
1705                            .is_err(),
1706                    )
1707                },
1708            )
1709            .collect::<Vec<_>>();
1710        let expected = cases
1711            .iter()
1712            .map(|&(name, ..)| (name, true))
1713            .collect::<Vec<_>>();
1714        assert2::assert!(rejected == expected);
1715
1716        // Kafka's `ExponentialBackoff` accepts an initial backoff above the
1717        // maximum, and uses the maximum.
1718        assert2::assert!(
1719            ProducerRetryPolicy::new(one, 0, Duration::from_millis(2), one, one, one).is_ok()
1720        );
1721    }
1722
1723    #[test]
1724    fn producer_retry_policy_names_invalid_retry_duration() {
1725        let valid = Duration::from_millis(1);
1726        let oversized = Duration::from_millis(i32::MAX as u64 + 1);
1727        let error = |backoff, max, init| {
1728            ProducerRetryPolicy::new(valid, 0, backoff, max, init, valid)
1729                .expect_err("invalid retry duration")
1730        };
1731
1732        let messages = [
1733            error(oversized, valid, valid),
1734            error(valid, oversized, valid),
1735            error(valid, valid, oversized),
1736        ]
1737        .map(|message| message.split(':').next().unwrap_or_default().to_owned());
1738        assert2::assert!(
1739            messages
1740                == [
1741                    "producer retry backoff",
1742                    "producer retry backoff maximum",
1743                    "producer-ID initialization retry timeout",
1744                ]
1745        );
1746    }
1747
1748    /// Kafka's `KafkaProducer.configureDeliveryTimeout` requires
1749    /// `delivery.timeout.ms >= linger.ms + request.timeout.ms`. A configured
1750    /// value below it is an error, and the default grows to it.
1751    #[test]
1752    fn delivery_timeout_follows_kafka_rule() {
1753        let ms = Duration::from_millis;
1754        let rule = "delivery_timeout should be equal to or larger than linger + request_timeout";
1755        let cases = [
1756            ("default", None, ms(5), ms(30_000), Ok(ms(120_000))),
1757            (
1758                "configured equal",
1759                Some(ms(30_005)),
1760                ms(5),
1761                ms(30_000),
1762                Ok(ms(30_005)),
1763            ),
1764            (
1765                "configured below",
1766                Some(ms(30_004)),
1767                ms(5),
1768                ms(30_000),
1769                Err(rule.to_owned()),
1770            ),
1771            ("default below", None, ms(5), ms(200_000), Ok(ms(200_005))),
1772            (
1773                "sum above i32 range",
1774                None,
1775                ms(i32::MAX as u64),
1776                ms(i32::MAX as u64),
1777                Ok(ms(i32::MAX as u64)),
1778            ),
1779        ];
1780        let actual = cases
1781            .iter()
1782            .map(|(name, configured, linger, request, _)| {
1783                (
1784                    *name,
1785                    resolve_delivery_timeout(*configured, *linger, *request),
1786                )
1787            })
1788            .collect::<Vec<_>>();
1789        let expected = cases
1790            .into_iter()
1791            .map(|(name, .., want)| (name, want))
1792            .collect::<Vec<_>>();
1793        assert2::assert!(actual == expected);
1794    }
1795
1796    /// A configured delivery timeout below `linger + request_timeout` fails
1797    /// `build()`. The default linger of 5 ms counts in the sum, as in Kafka.
1798    #[tokio::test]
1799    async fn producer_builder_rejects_delivery_timeout_below_linger_and_request_timeout() {
1800        let rule = "invalid config: delivery_timeout should be equal to or larger than linger \
1801                    + request_timeout";
1802        let cases = [
1803            ("configured linger", Some(Duration::from_millis(5)), 1_004),
1804            ("default linger", None, 1_004),
1805            ("default linger, delivery equal to request", None, 1_000),
1806        ];
1807        let mut actual = Vec::with_capacity(cases.len());
1808        let mut expected = Vec::with_capacity(cases.len());
1809        for (name, linger, delivery_timeout_ms) in cases {
1810            let error = Producer::builder()
1811                .bootstrap("127.0.0.1:1")
1812                .maybe_linger(linger)
1813                .request_timeout(Duration::from_secs(1))
1814                .delivery_timeout(Duration::from_millis(delivery_timeout_ms))
1815                .build()
1816                .await
1817                .err()
1818                .map(|error| error.to_string());
1819            actual.push((name, error));
1820            expected.push((name, Some(rule.to_owned())));
1821        }
1822        assert2::assert!(actual == expected);
1823    }
1824
1825    #[test]
1826    fn init_producer_id_request_is_idempotent_only_shape() {
1827        let req = build_init_producer_id_request();
1828
1829        assert2::assert!((req.transactional_id.as_ref(), req.transaction_timeout_ms) == (None, 0));
1830    }
1831
1832    #[test]
1833    fn retry_backoff_doubles_and_caps() {
1834        for (_name, current, want) in [
1835            (
1836                "double below cap",
1837                Duration::from_millis(100),
1838                Duration::from_millis(200),
1839            ),
1840            (
1841                "double to cap",
1842                Duration::from_millis(800),
1843                Duration::from_secs(1),
1844            ),
1845            (
1846                "remain capped",
1847                Duration::from_secs(1),
1848                Duration::from_secs(1),
1849            ),
1850        ] {
1851            assert2::assert!(next_backoff(current, Duration::from_secs(1)) == want);
1852        }
1853    }
1854
1855    /// The settings that Kafka's `ProducerConfig.postProcessParsedConfig`
1856    /// derives from the producer inputs.
1857    #[derive(Debug, PartialEq, Eq)]
1858    struct ResolvedConfig {
1859        client_id: String,
1860        enable_idempotence: bool,
1861        acks: Acks,
1862        linger: Duration,
1863    }
1864
1865    /// The inputs of one builder case. `None` leaves the setter uncalled.
1866    #[derive(Default)]
1867    struct BuilderInputs {
1868        client_id: Option<&'static str>,
1869        enable_idempotence: Option<bool>,
1870        acks: Option<Acks>,
1871        retries: Option<i32>,
1872        max_in_flight: Option<usize>,
1873        transactional_id: Option<&'static str>,
1874    }
1875
1876    /// Replaces the sequence number of a generated `producer-<n>` client id,
1877    /// because tests that run in parallel also take numbers.
1878    fn without_sequence(client_id: String) -> String {
1879        match client_id.strip_prefix("producer-") {
1880            Some(rest) if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) => {
1881                "producer-<n>".to_owned()
1882            }
1883            _ => client_id,
1884        }
1885    }
1886
1887    async fn resolved_config(
1888        bootstrap: &str,
1889        inputs: BuilderInputs,
1890    ) -> Result<ResolvedConfig, String> {
1891        let result = Producer::builder()
1892            .bootstrap(bootstrap)
1893            .maybe_client_id(inputs.client_id)
1894            .maybe_enable_idempotence(inputs.enable_idempotence)
1895            .maybe_acks(inputs.acks)
1896            .maybe_retries(inputs.retries)
1897            .maybe_max_in_flight_per_connection(inputs.max_in_flight)
1898            .maybe_transactional_id(inputs.transactional_id)
1899            .request_timeout(Duration::from_millis(500))
1900            .build()
1901            .await;
1902        match result {
1903            Ok(producer) => {
1904                let resolved = ResolvedConfig {
1905                    client_id: without_sequence(producer.client_id.clone()),
1906                    enable_idempotence: producer.producer_id() >= 0,
1907                    acks: producer.acks,
1908                    linger: producer.linger.to_std(),
1909                };
1910                producer.close().await.expect("close producer");
1911                Ok(resolved)
1912            }
1913            Err(ProducerError::InvalidConfig(message)) => Err(message),
1914            Err(error) => panic!("unexpected build error: {error:?}"),
1915        }
1916    }
1917
1918    /// The builder applies Kafka's producer defaults and the rules of
1919    /// `ProducerConfig.postProcessAndValidateIdempotenceConfigs` and
1920    /// `ProducerConfig.maybeOverrideClientId`.
1921    #[tokio::test]
1922    async fn producer_builder_resolves_kafka_defaults_and_idempotence_rules() {
1923        let mock = MockBroker::start(|api_key, _version, _corr_id, _body| {
1924            if api_key == api_versions_request::API_KEY {
1925                return Some(encode_v0(&ApiVersionsResponse::default()));
1926            }
1927            (api_key == init_producer_id_request::API_KEY).then(|| {
1928                encode_v0(&InitProducerIdResponse {
1929                    producer_id: 1,
1930                    ..Default::default()
1931                })
1932            })
1933        })
1934        .await
1935        .unwrap();
1936        let bootstrap = mock.addr.to_string();
1937        let five_ms = Duration::from_millis(5);
1938        let acks_error = "Must set acks to all in order to use the idempotent producer. \
1939                          Otherwise we cannot guarantee idempotence.";
1940        let retries_error = "Must set retries to non-zero when using the idempotent producer.";
1941        let in_flight_error = "To use the idempotent producer, max_in_flight_per_connection \
1942                               must be set to at most 5. Current value is 6.";
1943        let transactional_error =
1944            "Cannot set a transactional_id without also enabling idempotence.";
1945        let cases: Vec<(&str, BuilderInputs, Result<ResolvedConfig, String>)> = vec![
1946            (
1947                "nothing set",
1948                BuilderInputs::default(),
1949                Ok(ResolvedConfig {
1950                    client_id: "producer-<n>".to_owned(),
1951                    enable_idempotence: true,
1952                    acks: Acks::All,
1953                    linger: five_ms,
1954                }),
1955            ),
1956            (
1957                "idempotence set, acks one",
1958                BuilderInputs {
1959                    enable_idempotence: Some(true),
1960                    acks: Some(Acks::One),
1961                    ..BuilderInputs::default()
1962                },
1963                Err(acks_error.to_owned()),
1964            ),
1965            (
1966                "idempotence set, acks zero",
1967                BuilderInputs {
1968                    enable_idempotence: Some(true),
1969                    acks: Some(Acks::Zero),
1970                    ..BuilderInputs::default()
1971                },
1972                Err(acks_error.to_owned()),
1973            ),
1974            (
1975                "idempotence not set, acks one",
1976                BuilderInputs {
1977                    acks: Some(Acks::One),
1978                    ..BuilderInputs::default()
1979                },
1980                Ok(ResolvedConfig {
1981                    client_id: "producer-<n>".to_owned(),
1982                    enable_idempotence: false,
1983                    acks: Acks::One,
1984                    linger: five_ms,
1985                }),
1986            ),
1987            (
1988                "idempotence not set, acks zero",
1989                BuilderInputs {
1990                    acks: Some(Acks::Zero),
1991                    ..BuilderInputs::default()
1992                },
1993                Ok(ResolvedConfig {
1994                    client_id: "producer-<n>".to_owned(),
1995                    enable_idempotence: false,
1996                    acks: Acks::Zero,
1997                    linger: five_ms,
1998                }),
1999            ),
2000            (
2001                "idempotence set, retries zero",
2002                BuilderInputs {
2003                    enable_idempotence: Some(true),
2004                    acks: Some(Acks::All),
2005                    retries: Some(0),
2006                    ..BuilderInputs::default()
2007                },
2008                Err(retries_error.to_owned()),
2009            ),
2010            (
2011                "idempotence not set, retries zero",
2012                BuilderInputs {
2013                    acks: Some(Acks::All),
2014                    retries: Some(0),
2015                    ..BuilderInputs::default()
2016                },
2017                Ok(ResolvedConfig {
2018                    client_id: "producer-<n>".to_owned(),
2019                    enable_idempotence: false,
2020                    acks: Acks::All,
2021                    linger: five_ms,
2022                }),
2023            ),
2024            (
2025                "idempotence set, six in flight",
2026                BuilderInputs {
2027                    enable_idempotence: Some(true),
2028                    acks: Some(Acks::All),
2029                    max_in_flight: Some(6),
2030                    ..BuilderInputs::default()
2031                },
2032                Err(in_flight_error.to_owned()),
2033            ),
2034            (
2035                "idempotence not set, six in flight",
2036                BuilderInputs {
2037                    max_in_flight: Some(6),
2038                    ..BuilderInputs::default()
2039                },
2040                Err(in_flight_error.to_owned()),
2041            ),
2042            (
2043                "idempotence not set, retries zero, six in flight",
2044                BuilderInputs {
2045                    retries: Some(0),
2046                    max_in_flight: Some(6),
2047                    ..BuilderInputs::default()
2048                },
2049                Err(in_flight_error.to_owned()),
2050            ),
2051            (
2052                "idempotence off, six in flight",
2053                BuilderInputs {
2054                    enable_idempotence: Some(false),
2055                    max_in_flight: Some(6),
2056                    ..BuilderInputs::default()
2057                },
2058                Ok(ResolvedConfig {
2059                    client_id: "producer-<n>".to_owned(),
2060                    enable_idempotence: false,
2061                    acks: Acks::All,
2062                    linger: five_ms,
2063                }),
2064            ),
2065            (
2066                "idempotence off, transactional id",
2067                BuilderInputs {
2068                    enable_idempotence: Some(false),
2069                    acks: Some(Acks::All),
2070                    transactional_id: Some("t"),
2071                    ..BuilderInputs::default()
2072                },
2073                Err(transactional_error.to_owned()),
2074            ),
2075            (
2076                "idempotence turned off by acks, transactional id",
2077                BuilderInputs {
2078                    acks: Some(Acks::One),
2079                    transactional_id: Some("t"),
2080                    ..BuilderInputs::default()
2081                },
2082                Err(transactional_error.to_owned()),
2083            ),
2084            (
2085                "transactional id names the client",
2086                BuilderInputs {
2087                    transactional_id: Some("t"),
2088                    ..BuilderInputs::default()
2089                },
2090                Ok(ResolvedConfig {
2091                    client_id: "producer-t".to_owned(),
2092                    enable_idempotence: true,
2093                    acks: Acks::All,
2094                    linger: five_ms,
2095                }),
2096            ),
2097            (
2098                "configured client id wins",
2099                BuilderInputs {
2100                    client_id: Some("app"),
2101                    transactional_id: Some("t"),
2102                    ..BuilderInputs::default()
2103                },
2104                Ok(ResolvedConfig {
2105                    client_id: "app".to_owned(),
2106                    enable_idempotence: true,
2107                    acks: Acks::All,
2108                    linger: five_ms,
2109                }),
2110            ),
2111        ];
2112        let mut actual = Vec::with_capacity(cases.len());
2113        let mut expected = Vec::with_capacity(cases.len());
2114        for (name, inputs, want) in cases {
2115            actual.push((name, resolved_config(&bootstrap, inputs).await));
2116            expected.push((name, want));
2117        }
2118        mock.stop();
2119        assert2::assert!(actual == expected);
2120    }
2121
2122    #[test]
2123    fn generated_client_ids_take_distinct_sequence_numbers() {
2124        let first = resolve_client_id(None, None);
2125        let second = resolve_client_id(None, None);
2126        let number = |id: &str| {
2127            id.strip_prefix("producer-")
2128                .and_then(|n| n.parse::<u64>().ok())
2129                .expect("generated client id")
2130        };
2131        assert2::assert!(number(&second) > number(&first));
2132    }
2133
2134    #[test]
2135    fn disabled_idempotence_identity_uses_kafka_sentinel_values() {
2136        assert2::assert!(disabled_idempotence_identity() == (-1, -1));
2137        assert2::assert!(initial_txn_pid_epoch() == (-1, -1));
2138    }
2139
2140    #[test]
2141    fn producer_identity_from_init_maps_error_and_success() {
2142        let success = InitProducerIdResponse {
2143            error_code: 0,
2144            producer_id: 42,
2145            producer_epoch: 7,
2146            ..Default::default()
2147        };
2148        assert2::assert!(producer_identity_from_init(&success).unwrap() == (42, 7));
2149
2150        let error = InitProducerIdResponse {
2151            error_code: 51,
2152            ..Default::default()
2153        };
2154        assert2::assert!(matches!(
2155            producer_identity_from_init(&error),
2156            Err(ProducerError::Server(51))
2157        ));
2158    }
2159
2160    #[tokio::test]
2161    async fn producer_builder_accepts_security() {
2162        let security = ClientSecurity {
2163            protocol: ListenerProtocol::SaslPlaintext,
2164            tls: None,
2165            sasl: Some(SaslCredentials::Plain {
2166                username: "u".into(),
2167                password: "p".into(),
2168            }),
2169            sasl_host: None,
2170        };
2171        // 127.0.0.1:1 is unroutable for a listener; with idempotence on the
2172        // build issues an InitProducerId, which must fail at connect —
2173        // proving the security arg is threaded (not a type error). A lazy
2174        // (non-idempotent) build would not connect, so keep idempotence on.
2175        let failed = Producer::builder()
2176            .bootstrap("127.0.0.1:1")
2177            .request_timeout(Duration::from_millis(500))
2178            .security(security)
2179            .build()
2180            .await
2181            .is_err();
2182        assert2::assert!(failed);
2183    }
2184
2185    #[tokio::test]
2186    async fn producer_builder_uses_request_timeout_for_initial_connect_handshake() {
2187        let mock = MockBroker::start(|_api_key, _version, _corr_id, _body| None)
2188            .await
2189            .unwrap();
2190
2191        let build = Producer::builder()
2192            .bootstrap(mock.addr.to_string())
2193            .request_timeout(Duration::from_millis(100))
2194            .build();
2195        let res = time::timeout(Duration::from_millis(500), build)
2196            .await
2197            .map(Result::err);
2198
2199        mock.stop();
2200
2201        let err = res
2202            .expect("producer build should not retain the 30s connect timeout")
2203            .expect("silent broker must time out during build");
2204        assert2::assert!(matches!(
2205            err,
2206            ProducerError::Client(ClientError::Timeout(d))
2207                if d == krabka_units::millis(100)
2208        ));
2209    }
2210
2211    #[tokio::test]
2212    async fn producer_builder_uses_configured_init_retry_timeout() {
2213        let attempts = Arc::new(AtomicUsize::new(0));
2214        let observed = Arc::clone(&attempts);
2215        let mock = MockBroker::start(move |api_key, _version, _corr_id, _body| {
2216            if api_key == api_versions_request::API_KEY {
2217                return Some(encode_v0(&ApiVersionsResponse::default()));
2218            }
2219            if api_key == init_producer_id_request::API_KEY {
2220                observed.fetch_add(1, Ordering::Relaxed);
2221                return Some(encode_v0(&InitProducerIdResponse {
2222                    error_code: 14, // COORDINATOR_LOAD_IN_PROGRESS
2223                    ..Default::default()
2224                }));
2225            }
2226            None
2227        })
2228        .await
2229        .unwrap();
2230
2231        let build = Producer::builder()
2232            .bootstrap(mock.addr.to_string())
2233            .request_timeout(Duration::from_millis(100))
2234            .retry_backoff(Duration::from_millis(10))
2235            .retry_backoff_max(Duration::from_millis(10))
2236            .init_retry_timeout(Duration::from_millis(100))
2237            .build();
2238        let error = time::timeout(Duration::from_millis(500), build)
2239            .await
2240            .expect("configured init retry timeout must bound the build")
2241            .expect_err("cold coordinator must remain an error");
2242
2243        mock.stop();
2244        // 14 is COORDINATOR_LOAD_IN_PROGRESS.
2245        assert2::assert!(matches!(error, ProducerError::Server(14)));
2246        assert2::assert!(attempts.load(Ordering::Relaxed) >= 1);
2247    }
2248
2249    #[tokio::test]
2250    async fn two_phase_init_fields_require_version_six_before_dispatch() {
2251        for (name, request) in [
2252            (
2253                "enable 2PC",
2254                InitProducerIdRequest {
2255                    enable2_pc: true,
2256                    ..Default::default()
2257                },
2258            ),
2259            (
2260                "keep prepared transaction",
2261                InitProducerIdRequest {
2262                    keep_prepared_txn: true,
2263                    ..Default::default()
2264                },
2265            ),
2266        ] {
2267            let attempts = Arc::new(AtomicUsize::new(0));
2268            let observed = Arc::clone(&attempts);
2269            let mock = MockBroker::start(move |api_key, _version, _corr_id, _body| {
2270                if api_key == api_versions_request::API_KEY {
2271                    return Some(encode_v0(&ApiVersionsResponse {
2272                        api_keys: vec![ApiVersion {
2273                            api_key: init_producer_id_request::API_KEY,
2274                            min_version: 0,
2275                            max_version: 5,
2276                            ..Default::default()
2277                        }],
2278                        ..Default::default()
2279                    }));
2280                }
2281                if api_key == init_producer_id_request::API_KEY {
2282                    observed.fetch_add(1, Ordering::Relaxed);
2283                }
2284                None
2285            })
2286            .await
2287            .unwrap();
2288            let client = Client::builder()
2289                .bootstrap(mock.addr.to_string())
2290                .request_timeout(millis(100))
2291                .build()
2292                .await
2293                .unwrap();
2294
2295            let error =
2296                init_producer_id_with_retry(&client, request, millis(100), millis(1), millis(1))
2297                    .await
2298                    .expect_err(name);
2299
2300            assert2::assert!(matches!(
2301                error,
2302                ProducerError::Client(ClientError::IncompatibleVersion {
2303                    api_key: init_producer_id_request::API_KEY,
2304                    broker_min: 0,
2305                    broker_max: 5,
2306                    client_min: 6,
2307                    client_max: 6,
2308                })
2309            ));
2310            assert2::assert!(attempts.load(Ordering::Relaxed) == 0, "{name}");
2311            mock.stop();
2312        }
2313    }
2314
2315    /// A request without the two-phase commit fields negotiates v5 at most, as
2316    /// Kafka's producer does. A two-phase commit request still sends v6.
2317    #[tokio::test]
2318    async fn init_producer_id_sends_the_unstable_version_only_for_two_phase_commit() {
2319        const CLIENT_ID: &str = "p";
2320        let transactional = InitProducerIdRequest {
2321            transactional_id: Some("txn".into()),
2322            transaction_timeout_ms: 60_000,
2323            ..Default::default()
2324        };
2325        let two_phase = InitProducerIdRequest {
2326            enable2_pc: true,
2327            ..transactional.clone()
2328        };
2329        let cases = [
2330            (
2331                "idempotent, broker max 3",
2332                build_init_producer_id_request(),
2333                3,
2334                3,
2335            ),
2336            (
2337                "idempotent, broker max 5",
2338                build_init_producer_id_request(),
2339                5,
2340                5,
2341            ),
2342            (
2343                "idempotent, broker max 6",
2344                build_init_producer_id_request(),
2345                6,
2346                5,
2347            ),
2348            ("transactional, broker max 6", transactional, 6, 5),
2349            ("two-phase commit, broker max 6", two_phase, 6, 6),
2350        ];
2351        for (name, request, broker_max, expected_version) in cases {
2352            let seen = Arc::new(StdMutex::new(Vec::new()));
2353            let handler_seen = Arc::clone(&seen);
2354            let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
2355                if api_key == api_versions_request::API_KEY {
2356                    return Some(encode_v0(&ApiVersionsResponse {
2357                        api_keys: vec![ApiVersion {
2358                            api_key: init_producer_id_request::API_KEY,
2359                            min_version: 0,
2360                            max_version: broker_max,
2361                            ..Default::default()
2362                        }],
2363                        ..Default::default()
2364                    }));
2365                }
2366                if api_key == init_producer_id_request::API_KEY {
2367                    let flexible = version >= init_producer_id_request::FLEXIBLE_MIN;
2368                    let mut request_body = &body[2 + CLIENT_ID.len() + usize::from(flexible)..];
2369                    let decoded = InitProducerIdRequest::decode(&mut request_body, version)
2370                        .expect("decode InitProducerId");
2371                    handler_seen.lock().unwrap().push((version, decoded));
2372                    let mut response = BytesMut::new();
2373                    if version >= init_producer_id_response::FLEXIBLE_MIN {
2374                        response.extend_from_slice(&[0]);
2375                    }
2376                    InitProducerIdResponse {
2377                        producer_id: 1,
2378                        ..Default::default()
2379                    }
2380                    .encode(&mut response, version)
2381                    .expect("encode InitProducerId response");
2382                    return Some(response.to_vec());
2383                }
2384                None
2385            })
2386            .await
2387            .unwrap();
2388            let client = Client::builder()
2389                .bootstrap(mock.addr.to_string())
2390                .client_id(CLIENT_ID)
2391                .request_timeout(millis(500))
2392                .build()
2393                .await
2394                .unwrap();
2395
2396            init_producer_id_with_retry(
2397                &client,
2398                request.clone(),
2399                millis(500),
2400                millis(1),
2401                millis(1),
2402            )
2403            .await
2404            .expect(name);
2405
2406            let sent = seen.lock().unwrap().clone();
2407            assert2::assert!(sent == vec![(expected_version, request)], "{name}");
2408            mock.stop();
2409        }
2410    }
2411
2412    #[tokio::test]
2413    async fn init_retry_timeout_bounds_an_unresponsive_request() {
2414        let mock = MockBroker::start(|api_key, _version, _corr_id, _body| {
2415            (api_key == api_versions_request::API_KEY)
2416                .then(|| encode_v0(&ApiVersionsResponse::default()))
2417        })
2418        .await
2419        .unwrap();
2420
2421        let build = Producer::builder()
2422            .bootstrap(mock.addr.to_string())
2423            .request_timeout(Duration::from_secs(5))
2424            .retry_backoff(Duration::from_millis(1))
2425            .retry_backoff_max(Duration::from_millis(1))
2426            .init_retry_timeout(Duration::from_millis(10))
2427            .build();
2428        let error = time::timeout(Duration::from_millis(200), build)
2429            .await
2430            .expect("init retry timeout must bound an in-flight request")
2431            .expect_err("unresponsive InitProducerId must time out");
2432
2433        mock.stop();
2434        assert2::assert!(matches!(
2435            error,
2436            ProducerError::Client(ClientError::Timeout(timeout))
2437                if timeout == krabka_units::millis(10)
2438        ));
2439    }
2440
2441    #[tokio::test]
2442    async fn producer_builder_rejects_retry_policy_before_connection_io() {
2443        macro_rules! invalid {
2444            ($setter:ident, $value:expr) => {
2445                Producer::builder()
2446                    .bootstrap("127.0.0.1:1")
2447                    .$setter($value)
2448                    .build()
2449                    .await
2450                    .expect_err("invalid retry policy must fail before connection I/O")
2451            };
2452        }
2453
2454        let zero = "[the value must be equal to 1, but received 0 || the value must be greater than 1, but received 0]";
2455        for (error, expected) in [
2456            (
2457                invalid!(request_timeout, Duration::ZERO),
2458                format!("invalid config: request timeout: {zero}"),
2459            ),
2460            (
2461                invalid!(retries, -1),
2462                "invalid config: producer retries: the value must be greater than -1, but received -1"
2463                    .to_owned(),
2464            ),
2465            (
2466                invalid!(retry_backoff, Duration::ZERO),
2467                format!("invalid config: producer retry backoff: {zero}"),
2468            ),
2469            (
2470                invalid!(delivery_timeout, Duration::ZERO),
2471                format!("invalid config: delivery timeout: {zero}"),
2472            ),
2473            (
2474                invalid!(init_retry_timeout, Duration::ZERO),
2475                format!("invalid config: producer-ID initialization retry timeout: {zero}"),
2476            ),
2477            (
2478                invalid!(retry_backoff_max, Duration::ZERO),
2479                format!("invalid config: producer retry backoff maximum: {zero}"),
2480            ),
2481            (
2482                invalid!(transaction_timeout, Duration::ZERO),
2483                format!("invalid config: transaction timeout: {zero}"),
2484            ),
2485        ] {
2486            let message = error.to_string();
2487            assert2::assert!(message == expected);
2488        }
2489    }
2490
2491    #[tokio::test]
2492    async fn two_phase_commit_rejects_conflicting_or_incomplete_configuration() {
2493        let conflict = Producer::builder()
2494            .bootstrap("127.0.0.1:1")
2495            .transactional_id("payments")
2496            .transaction_two_phase_commit_enable(true)
2497            .transaction_timeout(Duration::from_secs(30))
2498            .build()
2499            .await
2500            .expect_err("2PC owns the transaction timeout");
2501        assert2::assert!(matches!(
2502            conflict,
2503            ProducerError::InvalidConfig(message)
2504                if message == "transaction_timeout cannot be set when transaction_two_phase_commit_enable=true"
2505        ));
2506
2507        let missing_id = Producer::builder()
2508            .bootstrap("127.0.0.1:1")
2509            .transaction_two_phase_commit_enable(true)
2510            .build()
2511            .await
2512            .expect_err("2PC requires a transactional id");
2513        assert2::assert!(matches!(
2514            missing_id,
2515            ProducerError::InvalidConfig(message)
2516                if message == "transaction_two_phase_commit_enable=true requires transactional_id"
2517        ));
2518    }
2519
2520    #[tokio::test]
2521    async fn producer_builder_rejects_invalid_dns_timeout_before_connection_io() {
2522        for timeout in [Time::ZERO, micros(1), Time::from_secs_f64(f64::INFINITY)] {
2523            let error = Producer::builder()
2524                .bootstrap("127.0.0.1:1")
2525                .dns_timeout(timeout)
2526                .build()
2527                .await
2528                .expect_err("invalid DNS timeout must fail before connection I/O");
2529            assert2::assert!(matches!(
2530                error,
2531                ProducerError::InvalidConfig(message)
2532                    if message.starts_with("client DNS timeout")
2533            ));
2534        }
2535    }
2536
2537    #[tokio::test]
2538    async fn producer_builder_rejects_invalid_metadata_rebootstrap_trigger_before_io() {
2539        for trigger in [Time::from_millis(-1), krabka_units::micros(1)] {
2540            let error = Producer::builder()
2541                .bootstrap("127.0.0.1:1")
2542                .metadata_recovery_rebootstrap_trigger(trigger)
2543                .build()
2544                .await
2545                .expect_err("invalid metadata recovery trigger");
2546            assert2::assert!(matches!(error, ProducerError::InvalidConfig(_)));
2547        }
2548    }
2549
2550    #[tokio::test]
2551    async fn producer_builder_accepts_a_distinct_dns_timeout() -> Result<(), ProducerError> {
2552        Producer::builder()
2553            .bootstrap("127.0.0.1:1")
2554            .dns_timeout(millis(37))
2555            .enable_idempotence(false)
2556            .build()
2557            .await
2558            .expect("valid DNS timeout")
2559            .close()
2560            .await
2561    }
2562
2563    /// `send.buffer.bytes` and `receive.buffer.bytes` reach the options that
2564    /// each broker socket of the producer gets before it connects, with
2565    /// Kafka's producer defaults of 128 KiB and 32 KiB, and `None` for
2566    /// Kafka's `-1`.
2567    #[tokio::test]
2568    async fn producer_builder_carries_socket_buffers_to_the_client() {
2569        let kib = krabka_units::kibibytes;
2570        for (name, send, receive, expected) in [
2571            ("defaults", None, None, (Some(kib(128)), Some(kib(32)))),
2572            (
2573                "set",
2574                Some(Some(kib(256))),
2575                Some(Some(kib(8))),
2576                (Some(kib(256)), Some(kib(8))),
2577            ),
2578            (
2579                "operating system default",
2580                Some(None),
2581                Some(None),
2582                (None, None),
2583            ),
2584        ] {
2585            let (options, socket_buffers) = socket_buffers_and_close(
2586                Producer::builder()
2587                    .bootstrap("127.0.0.1:1")
2588                    .enable_idempotence(false)
2589                    .maybe_send_buffer(send)
2590                    .maybe_receive_buffer(receive)
2591                    .build()
2592                    .await
2593                    .expect("valid socket buffers"),
2594            )
2595            .await
2596            .expect("close producer");
2597            assert2::assert!(options == expected, "case {name}");
2598            assert2::assert!(
2599                socket_buffers
2600                    == producer::SocketBuffers {
2601                        send: expected.0,
2602                        receive: expected.1,
2603                    },
2604                "case {name}"
2605            );
2606        }
2607    }
2608
2609    /// The socket buffers of `producer`: the ones its client's connection
2610    /// options carry, and the ones it keeps itself. The producer is closed.
2611    async fn socket_buffers_and_close(
2612        producer: Producer,
2613    ) -> Result<
2614        (
2615            (Option<ByteSize>, Option<ByteSize>),
2616            producer::SocketBuffers,
2617        ),
2618        ProducerError,
2619    > {
2620        let options = producer.client.connection_options();
2621        let observed = (
2622            (options.send_buffer, options.receive_buffer),
2623            producer.socket_buffers,
2624        );
2625        producer.close().await.map(|()| observed)
2626    }
2627
2628    #[tokio::test]
2629    async fn producer_builder_carries_client_resource_policy() -> Result<(), ProducerError> {
2630        let producer = Producer::builder()
2631            .bootstrap("127.0.0.1:1")
2632            .dispatch_queue_capacity(7)
2633            .frame_max(krabka_units::kibibytes(32))
2634            .enable_idempotence(false)
2635            .build()
2636            .await
2637            .expect("valid client resource policy");
2638
2639        assert2::assert!(producer.dispatch_queue_capacity.get() == 7);
2640        assert2::assert!(producer.frame_max.size() == krabka_units::kibibytes(32));
2641        producer.close().await
2642    }
2643
2644    #[tokio::test]
2645    async fn producer_builder_rejects_flush_timeout_before_connection_io() {
2646        macro_rules! invalid {
2647            ($setter:ident, $value:expr, $field:literal) => {
2648                let error = Producer::builder()
2649                    .bootstrap("127.0.0.1:1")
2650                    .$setter($value)
2651                    .build()
2652                    .await
2653                    .expect_err("invalid flush timeout must fail before connection I/O");
2654                assert!(
2655                    matches!(
2656                        error,
2657                        ProducerError::InvalidConfig(ref message) if message.starts_with($field)
2658                    ),
2659                    "{error:?} does not name {:?}",
2660                    $field
2661                );
2662            };
2663        }
2664
2665        invalid!(flush_timeout, Duration::ZERO, "producer flush timeout");
2666    }
2667
2668    #[tokio::test]
2669    async fn producer_builder_rejects_throughput_policy_before_connection_io() {
2670        macro_rules! invalid {
2671            ($setter:ident, $value:expr) => {
2672                Producer::builder()
2673                    .bootstrap("127.0.0.1:1")
2674                    .$setter($value)
2675                    .build()
2676                    .await
2677                    .expect_err("invalid throughput policy must fail before connection I/O")
2678            };
2679        }
2680
2681        for (error, field) in [
2682            (invalid!(linger, Duration::from_nanos(1)), "producer linger"),
2683            (
2684                invalid!(linger, Duration::from_millis(i32::MAX as u64 + 1)),
2685                "producer linger",
2686            ),
2687            (invalid!(batch_size, 0), "producer batch bytes"),
2688            (
2689                invalid!(batch_size, i32::MAX as usize + 1),
2690                "producer batch bytes",
2691            ),
2692            (
2693                invalid!(max_in_flight_per_connection, 0),
2694                "producer max in flight",
2695            ),
2696            (
2697                invalid!(max_request_size, i32::MAX as usize + 1),
2698                "producer max request size",
2699            ),
2700            (
2701                invalid!(compression_gzip_level, 0),
2702                "Invalid value 0 for configuration compression_gzip_level",
2703            ),
2704            (
2705                invalid!(compression_lz4_level, 18),
2706                "Invalid value 18 for configuration compression_lz4_level",
2707            ),
2708            (
2709                invalid!(compression_zstd_level, 23),
2710                "Invalid value 23 for configuration compression_zstd_level",
2711            ),
2712        ] {
2713            assert!(
2714                matches!(
2715                    error,
2716                    ProducerError::InvalidConfig(ref message) if message.starts_with(field)
2717                ),
2718                "{error:?} does not name {field:?}"
2719            );
2720        }
2721    }
2722
2723    /// A build whose `InitProducerId` cannot connect returns the error to its
2724    /// caller, so no layer of it records the failure at ERROR
2725    /// (krabka-io/krabka-broker#1515).
2726    #[tokio::test]
2727    async fn a_build_that_cannot_connect_records_no_error() {
2728        let (result, levels) = recorded_levels(Box::pin(
2729            Producer::builder()
2730                .bootstrap("127.0.0.1:1")
2731                .request_timeout(Duration::from_millis(500))
2732                .build(),
2733        ))
2734        .await;
2735        assert2::assert!(result.is_err());
2736        assert2::check!(!levels.is_empty());
2737        assert2::check!(!levels.contains(&tracing::Level::ERROR), "{levels:?}");
2738    }
2739}