1use 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
59const INIT_PRODUCER_ID_2PC_MIN_VERSION: i16 = 6;
61const INIT_PRODUCER_ID_STABLE_MAX_VERSION: i16 = 5;
63
64#[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 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#[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
125pub const DEFAULT_PRODUCER_COMPRESSION: Compression = Compression::None;
127pub const DEFAULT_PRODUCER_LINGER: Duration = Duration::from_millis(5);
131pub const DEFAULT_PRODUCER_ACKS: Acks = Acks::All;
133const MAX_IN_FLIGHT_FOR_IDEMPOTENCE: usize = 5;
137
138static PRODUCER_CLIENT_ID_SEQUENCE: AtomicU64 = AtomicU64::new(1);
142pub const DEFAULT_PRODUCER_BATCH_BYTES: usize = 16 * 1024;
144pub const DEFAULT_PRODUCER_PARTITIONER_IGNORE_KEYS: bool = false;
147pub const DEFAULT_PRODUCER_PARTITIONER_ADAPTIVE_PARTITIONING_ENABLE: bool = true;
150pub const DEFAULT_PRODUCER_PARTITIONER_AVAILABILITY_TIMEOUT: Duration = Duration::ZERO;
154pub const DEFAULT_PRODUCER_PARTITIONER_RACK_AWARE: bool = false;
157pub const DEFAULT_PRODUCER_MAX_IN_FLIGHT: usize = 5;
159pub const DEFAULT_PRODUCER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
161pub const DEFAULT_PRODUCER_FLUSH_TIMEOUT: Duration = Duration::from_secs(50);
163pub const DEFAULT_PRODUCER_RETRIES: i32 = i32::MAX;
165pub const DEFAULT_PRODUCER_RETRY_BACKOFF: Duration = Duration::from_millis(100);
167pub const DEFAULT_PRODUCER_DELIVERY_TIMEOUT: Duration = Duration::from_mins(2);
170pub const DEFAULT_PRODUCER_INIT_RETRY_TIMEOUT: Duration = Duration::from_secs(30);
177pub const DEFAULT_PRODUCER_RETRY_BACKOFF_MAX: Duration = Duration::from_secs(1);
180pub const DEFAULT_PRODUCER_TRANSACTION_TIMEOUT: Duration = Duration::from_mins(1);
182pub const DEFAULT_PRODUCER_MAX_BLOCK: Duration = Duration::from_mins(1);
186pub const DEFAULT_PRODUCER_BUFFER_MEMORY: usize = 32 * 1024 * 1024;
190
191pub const DEFAULT_PRODUCER_MAX_REQUEST_SIZE: usize = 1024 * 1024;
195
196const SENDER_WAKE_CHANNEL_CAPACITY: usize = 16;
198
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub struct ProducerFlushTimeout(Duration);
202
203impl ProducerFlushTimeout {
204 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 fn default() -> Self {
228 Self(DEFAULT_PRODUCER_FLUSH_TIMEOUT)
229 }
230}
231
232#[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 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 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#[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 fn first_backoff(&self) -> Time {
335 self.retry_backoff().min(self.retry_backoff_max()).as_time()
336 }
337
338 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 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
434fn 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
452fn 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
505fn protocol_milliseconds(value: Duration) -> i32 {
509 i32::try_from(value.as_millis()).unwrap_or(i32::MAX)
510}
511
512#[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
526fn 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
589fn 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
624pub 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#[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
703fn 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
724pub const DEFAULT_PRODUCER_SEND_BUFFER: ByteSize = krabka_units::kibibytes(128);
726
727pub const DEFAULT_PRODUCER_RECEIVE_BUFFER: ByteSize = krabka_units::kibibytes(32);
730
731const PRODUCER_METADATA_SCOPE: krabka_client_core::MetadataScope =
734 krabka_client_core::MetadataScope::Topics {
735 allow_auto_topic_creation: true,
736 };
737
738struct 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
770struct 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
796fn 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 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#[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
928struct 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
955fn 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 #[builder(start_fn = builder, finish_fn = build)]
1136 #[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 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 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 #[builder(default)]
1178 client_dns_lookup: krabka_client_core::ClientDnsLookup,
1179 #[builder(default = krabka_client_core::DEFAULT_RECONNECT_BACKOFF)]
1181 reconnect_backoff: Time,
1182 #[builder(default = krabka_client_core::DEFAULT_RECONNECT_BACKOFF_MAX)]
1184 reconnect_backoff_max: Time,
1185 #[builder(default = krabka_client_core::DEFAULT_CONNECTIONS_MAX_IDLE)]
1187 connections_max_idle: Time,
1188 #[builder(default = krabka_client_core::DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT)]
1190 socket_connection_setup_timeout: Time,
1191 #[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 #[builder(required, default = Some(DEFAULT_PRODUCER_SEND_BUFFER))]
1201 send_buffer: Option<ByteSize>,
1202 #[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 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 let (producer_id, producer_epoch) =
1319 initial_producer_identity(&client, enable_idempotence, retry_policy, first_backoff)
1320 .await?;
1321
1322 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 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 #[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 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 #[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 #[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 #[derive(Debug, PartialEq, Eq)]
1858 struct ResolvedConfig {
1859 client_id: String,
1860 enable_idempotence: bool,
1861 acks: Acks,
1862 linger: Duration,
1863 }
1864
1865 #[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 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 #[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 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, ..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 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 #[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 #[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 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 #[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}