1use std::{
5 collections::HashMap,
6 fmt,
7 sync::{
8 Arc, Mutex as SyncMutex, MutexGuard as SyncMutexGuard, PoisonError,
9 atomic::{AtomicBool, AtomicI16, AtomicI64, AtomicU8, AtomicU64, AtomicUsize, Ordering},
10 },
11 time::{Duration, SystemTime, UNIX_EPOCH},
12};
13
14use bytes::BufMut;
15use dashmap::DashMap;
16use krabka_client_consumer::ConsumerGroupMetadata;
17use krabka_client_core::{
18 Client, ClientError, ClientFrameMax, ConnectionDispatchQueueCapacity, CoordinatorKeyType,
19 build_find_coordinator, security::ClientSecurity,
20};
21use krabka_protocol::{
22 Encode, ProtocolError, ProtocolRequest,
23 owned::{
24 add_offsets_to_txn_request::AddOffsetsToTxnRequest,
25 add_partitions_to_txn_request::{self, AddPartitionsToTxnRequest},
26 add_partitions_to_txn_response::AddPartitionsToTxnResponse,
27 common::add_partitions_to_txn_request::add_partitions_to_txn_topic::AddPartitionsToTxnTopic,
28 end_txn_request::EndTxnRequest,
29 find_coordinator_response::FindCoordinatorResponse,
30 init_producer_id_request::InitProducerIdRequest,
31 init_producer_id_response::InitProducerIdResponse,
32 metadata_response::MetadataResponse,
33 txn_offset_commit_request::{
34 TxnOffsetCommitRequest, TxnOffsetCommitRequestPartition, TxnOffsetCommitRequestTopic,
35 },
36 txn_offset_commit_response::TxnOffsetCommitResponse,
37 },
38 primitives::uuid::Uuid,
39};
40use krabka_units::{ByteSize, Time, convert::TimeExt};
41use tokio::{
42 sync::{Mutex, MutexGuard, Notify, mpsc},
43 task::JoinHandle,
44 time::{self, Instant},
45};
46use tokio_util::sync::CancellationToken;
47use tracing::{Instrument as _, field};
48
49use crate::{
50 accumulator::{
51 Accumulator, AccumulatorMap, AppendResult, approx_record_size, record_size_upper_bound,
52 },
53 buffer_pool::BufferPool,
54 builder::{ProducerFlushTimeout, send_init_producer_id},
55 compression::{Compression, CompressionLevels},
56 error::{ProducerError, RecordSizeLimit},
57 metadata_wait::{MetadataRefresh, MetadataWait, metadata_request},
58 partitioner::{
59 BuiltInPartitioner, PartitionInfo, Partitioner, RackPreference, StickyPartition,
60 TopicPartitions,
61 },
62 record::{DeliveryHandle, ProducerRecord, RecordMetadata},
63 sender::DrainIntent,
64 transactional::{
65 AbortableError, FatalError, OwnedTransaction, PreparedTransactionState, Transaction,
66 TxnErrorSlot, TxnState,
67 },
68 txn_retry::{
69 self, AddPartitionsDecision, CoordinatorAttempt, EndTxnDecision, FindCoordinatorDecision,
70 LookupRefusal, TxnRequestDecision,
71 },
72};
73
74const ADD_PARTITIONS_LAST_CLIENT_VERSION: i16 = 3;
76
77#[derive(Clone, Debug, PartialEq, Eq)]
87struct ClientAddPartitionsToTxn(AddPartitionsToTxnRequest);
88
89impl Encode for ClientAddPartitionsToTxn {
90 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
91 self.0.encode(buf, version)
92 }
93
94 fn encoded_len(&self, version: i16) -> usize {
95 self.0.encoded_len(version)
96 }
97}
98
99impl ProtocolRequest for ClientAddPartitionsToTxn {
100 const API_KEY: i16 = add_partitions_to_txn_request::API_KEY;
101 const MIN_VERSION: i16 = add_partitions_to_txn_request::MIN_VERSION;
102 const MAX_VERSION: i16 = ADD_PARTITIONS_LAST_CLIENT_VERSION;
103 const LATEST_STABLE_VERSION: i16 = Self::MAX_VERSION;
105 const FLEXIBLE_MIN: i16 = add_partitions_to_txn_request::FLEXIBLE_MIN;
106 type Response = AddPartitionsToTxnResponse;
107}
108
109const END_TXN_TRANSACTION_V1_MAX_VERSION: i16 = 4;
112
113const TXN_OFFSET_COMMIT_TRANSACTION_V1_MAX_VERSION: i16 = 4;
116
117const TXN_OFFSET_COMMIT_TOPIC_NAME_MAX_VERSION: i16 = 5;
121
122const TRANSACTION_VERSION_FEATURE: &str = "transaction.version";
124
125#[derive(Clone, Debug, PartialEq, Eq)]
132struct VersionCapped<R, const MAX: i16>(R);
133
134impl<R: Encode, const MAX: i16> Encode for VersionCapped<R, MAX> {
135 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
136 self.0.encode(buf, version)
137 }
138
139 fn encoded_len(&self, version: i16) -> usize {
140 self.0.encoded_len(version)
141 }
142}
143
144impl<R: ProtocolRequest, const MAX: i16> ProtocolRequest for VersionCapped<R, MAX> {
145 const API_KEY: i16 = R::API_KEY;
146 const MIN_VERSION: i16 = R::MIN_VERSION;
147 const MAX_VERSION: i16 = MAX;
148 const LATEST_STABLE_VERSION: i16 = MAX;
150 const FLEXIBLE_MIN: i16 = R::FLEXIBLE_MIN;
151 type Response = R::Response;
152}
153
154async fn send_in_transaction_version<R, const V1_MAX: i16>(
158 client: &Client,
159 request: R,
160 transaction_v2: bool,
161) -> Result<R::Response, ClientError>
162where
163 R: ProtocolRequest + Send + Sync,
164{
165 if transaction_v2 {
166 client.send(request).await
167 } else {
168 client.send(VersionCapped::<R, V1_MAX>(request)).await
169 }
170}
171
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181enum TxnOffsetCommitCap {
182 TransactionV1,
185 TopicNames,
188 Latest,
191}
192
193impl TxnOffsetCommitCap {
194 fn of(topics: &[TxnOffsetCommitRequestTopic], transaction_v2: bool) -> Self {
196 if !transaction_v2 {
197 Self::TransactionV1
198 } else if topics.iter().any(|topic| topic.topic_id == Uuid::ZERO) {
199 Self::TopicNames
200 } else {
201 Self::Latest
202 }
203 }
204
205 #[cfg(test)]
207 const fn max_version(self) -> i16 {
208 match self {
209 Self::TransactionV1 => TXN_OFFSET_COMMIT_TRANSACTION_V1_MAX_VERSION,
210 Self::TopicNames => TXN_OFFSET_COMMIT_TOPIC_NAME_MAX_VERSION,
211 Self::Latest => <TxnOffsetCommitRequest as ProtocolRequest>::LATEST_STABLE_VERSION,
212 }
213 }
214
215 async fn send(
218 self,
219 client: &Client,
220 request: TxnOffsetCommitRequest,
221 ) -> Result<TxnOffsetCommitResponse, ClientError> {
222 match self {
223 Self::TransactionV1 => {
224 client
225 .send(VersionCapped::<
226 _,
227 TXN_OFFSET_COMMIT_TRANSACTION_V1_MAX_VERSION,
228 >(request))
229 .await
230 }
231 Self::TopicNames => {
232 client
233 .send(VersionCapped::<_, TXN_OFFSET_COMMIT_TOPIC_NAME_MAX_VERSION>(request))
234 .await
235 }
236 Self::Latest => client.send(request).await,
237 }
238 }
239}
240
241fn transaction_v2_enabled(features: &krabka_client_core::FinalizedFeatures) -> bool {
245 features
246 .level(TRANSACTION_VERSION_FEATURE)
247 .is_some_and(|level| level >= 2)
248}
249
250struct CoordinatorRetry {
252 deadline: Instant,
253 backoff: Duration,
254 max_backoff: Duration,
255}
256
257impl CoordinatorRetry {
258 async fn wait(&mut self) -> bool {
262 let remaining = self.deadline.saturating_duration_since(Instant::now());
263 if remaining.is_zero() {
264 return false;
265 }
266 time::sleep(self.backoff.min(remaining)).await;
267 self.backoff = self.backoff.saturating_mul(2).min(self.max_backoff);
268 Instant::now() < self.deadline
269 }
270}
271
272#[derive(Debug, thiserror::Error)]
274enum LookupError {
275 #[error("the broker refused the coordinator lookup: {0:?}")]
277 Refused(LookupRefusal),
278 #[error("the coordinator lookup did not resolve: {0}")]
281 Unresolved(ProducerError),
282}
283
284impl From<ProducerError> for LookupError {
285 fn from(error: ProducerError) -> Self {
286 Self::Unresolved(error)
287 }
288}
289
290fn coordinator_answer(response: &FindCoordinatorResponse) -> (i16, String) {
296 response.coordinators.first().map_or_else(
297 || {
298 (
299 response.error_code,
300 format!("{}:{}", response.host, response.port),
301 )
302 },
303 |row| (row.error_code, format!("{}:{}", row.host, row.port)),
304 )
305}
306
307fn lookup_may_succeed_later(error: &ClientError) -> bool {
313 matches!(
314 error,
315 ClientError::Disconnected
316 | ClientError::Timeout(_)
317 | ClientError::Io(_)
318 | ClientError::Connect { .. }
319 | ClientError::Tls { .. }
320 | ClientError::Sasl { .. }
321 )
322}
323
324fn request_may_have_reached_the_broker(error: &ClientError) -> bool {
328 matches!(
329 error,
330 ClientError::Disconnected | ClientError::Timeout(_) | ClientError::Io(_)
331 )
332}
333
334fn txn_offset_commit_error_code(response: &TxnOffsetCommitResponse) -> i16 {
343 let codes = response
344 .topics
345 .iter()
346 .flat_map(|topic| {
347 topic
348 .partitions
349 .iter()
350 .map(|partition| partition.error_code)
351 })
352 .filter(|code| *code != 0);
353 let mut rediscover = None;
354 let mut resend = None;
355 for code in codes {
356 match txn_retry::decide_txn_offset_commit(CoordinatorAttempt::Answered(code)) {
357 TxnRequestDecision::Retry { rediscover: true } => {
358 rediscover.get_or_insert(code);
359 }
360 TxnRequestDecision::Retry { rediscover: false } => {
361 resend.get_or_insert(code);
362 }
363 _ => return code,
364 }
365 }
366 rediscover.or(resend).unwrap_or(0)
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum Acks {
371 Zero,
372 One,
373 All,
374}
375
376impl Acks {
377 #[must_use]
378 pub fn wire(self) -> i16 {
379 match self {
380 Self::Zero => 0,
381 Self::One => 1,
382 Self::All => -1,
383 }
384 }
385}
386
387pub const STATE_ACTIVE: u8 = 0;
389pub const STATE_FENCED: u8 = 1;
390pub const STATE_CLOSED: u8 = 2;
391
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct TopicMetadata {
394 pub num_partitions: i32,
395 pub topic_id: Uuid,
400}
401
402#[derive(Debug, Default)]
416pub struct MetadataCache(SyncMutex<HashMap<String, TopicMetadata>>);
417
418impl MetadataCache {
419 pub(crate) fn lock(&self) -> SyncMutexGuard<'_, HashMap<String, TopicMetadata>> {
421 self.0.lock().unwrap_or_else(PoisonError::into_inner)
427 }
428}
429
430impl From<HashMap<String, TopicMetadata>> for MetadataCache {
431 fn from(topics: HashMap<String, TopicMetadata>) -> Self {
432 Self(SyncMutex::new(topics))
433 }
434}
435
436#[derive(Clone, Debug)]
437pub struct ProducerIdentity {
438 pub id: Arc<AtomicI64>,
440 pub epoch: Arc<AtomicI16>,
443}
444
445fn wake_sender_after_append(wake_tx: &mpsc::Sender<DrainIntent>, linger: Time, wakes_sender: bool) {
446 if linger == <Time as TimeExt>::ZERO {
447 let _ = wake_tx.try_send(DrainIntent::Force);
448 } else if wakes_sender {
449 let _ = wake_tx.try_send(DrainIntent::Ready);
450 }
451}
452
453#[derive(Clone, Copy, Debug, PartialEq)]
456pub struct SocketBuffers {
457 pub(crate) send: Option<ByteSize>,
458 pub(crate) receive: Option<ByteSize>,
459}
460
461pub struct Producer {
463 pub(crate) client: Client,
464 pub(crate) client_id: String,
465 pub(crate) security: Option<ClientSecurity>,
474 pub(crate) dns_timeout: Time,
475 pub(crate) metadata_recovery_strategy: krabka_client_core::MetadataRecoveryStrategy,
476 pub(crate) metadata_recovery_rebootstrap_trigger:
477 krabka_client_core::MetadataRecoveryRebootstrapTrigger,
478 pub(crate) client_dns_lookup: krabka_client_core::ClientDnsLookup,
479 pub(crate) reconnect_backoff: Time,
480 pub(crate) reconnect_backoff_max: Time,
481 pub(crate) connections_max_idle: Time,
482 pub(crate) socket_connection_setup_timeout: Time,
483 pub(crate) socket_connection_setup_timeout_max: Time,
484 pub(crate) dispatch_queue_capacity: ConnectionDispatchQueueCapacity,
485 pub(crate) frame_max: ClientFrameMax,
486 pub(crate) socket_buffers: SocketBuffers,
489 pub(crate) identity: ProducerIdentity,
490 #[allow(dead_code)]
496 pub(crate) acks: Acks,
497 pub(crate) compression: Compression,
498 pub(crate) compression_levels: CompressionLevels,
500 pub(crate) batch_size: usize,
501 #[allow(dead_code)]
502 pub(crate) linger: Time,
503 #[allow(dead_code)]
504 pub(crate) request_timeout: Time,
505 pub(crate) flush_timeout: ProducerFlushTimeout,
506 pub(crate) max_block: Duration,
509 pub(crate) buffer_pool: BufferPool,
512 pub(crate) max_request_size: usize,
515 #[allow(dead_code)]
516 pub(crate) max_in_flight: usize,
517 pub(crate) metadata_cache: Arc<MetadataCache>,
518 pub(crate) metadata_refresh: Arc<MetadataRefresh>,
521 pub(crate) partition_leaders: Arc<DashMap<(String, i32), i32>>,
529 pub(crate) broker_racks: Arc<DashMap<i32, Option<String>>>,
533 pub(crate) accumulators: AccumulatorMap,
534 pub(crate) next_seq: Arc<DashMap<(String, i32), i32>>,
535 pub(crate) partitioner: Arc<BuiltInPartitioner>,
536 pub(crate) custom_partitioner: Option<Arc<dyn Partitioner>>,
540 pub(crate) client_rack: Option<String>,
544 pub(crate) state: Arc<AtomicU8>,
545 pub(crate) wake_tx: mpsc::Sender<DrainIntent>,
546 pub(crate) flush_notify: Arc<Notify>,
547 pub(crate) in_flight: Arc<AtomicUsize>,
557 pub(crate) sender_shutdown: CancellationToken,
558 pub(crate) sender_handle: Option<JoinHandle<()>>,
559 pub(crate) transactional_id: Option<String>,
560 pub(crate) transaction_timeout_ms: i32,
561 pub(crate) two_phase_commit_enabled: bool,
562 pub(crate) init_retry_timeout: Time,
563 pub(crate) init_retry_backoff: Time,
564 pub(crate) retry_backoff_max: Time,
565 pub(crate) txn_state: Arc<Mutex<TxnState>>,
568 pub(crate) txn_recovery_required: Arc<AtomicBool>,
572 pub(crate) txn_recovery_generation: Arc<AtomicU64>,
573 pub(crate) txn_guard_generation: Arc<AtomicU64>,
578 pub(crate) txn_coord_client: Mutex<Option<Client>>,
581 pub(crate) txn_pid_epoch: Arc<Mutex<(i64, i16)>>,
585 pub(crate) txn_error: Arc<TxnErrorSlot>,
595 pub(crate) transaction_v2: Arc<AtomicBool>,
602 pub(crate) prepared_transaction_state: Arc<Mutex<Option<PreparedTransactionState>>>,
607}
608
609fn minted_identity(pair: (i64, i16)) -> Option<(i64, i16)> {
617 let (id, epoch) = pair;
618 (id >= 0 && epoch >= 0).then_some(pair)
619}
620
621fn add_partitions_error_code(response: &AddPartitionsToTxnResponse) -> i16 {
630 response
631 .results_by_topic_v3_and_below
632 .first()
633 .or_else(|| {
634 let transaction = response.results_by_transaction.first()?;
635 transaction.topic_results.first()
636 })
637 .and_then(|topic| topic.results_by_partition.first())
638 .map_or(response.error_code, |partition| {
639 partition.partition_error_code
640 })
641}
642
643impl Producer {
644 pub(crate) async fn register_transaction_partition(
651 &self,
652 topic: &str,
653 partition: i32,
654 ) -> Result<(), ProducerError> {
655 let Some(transactional_id) = &self.transactional_id else {
656 return Ok(());
657 };
658 if self.transaction_v2() {
662 return Ok(());
663 }
664 let mut coordinator = self.txn_coord_client.lock().await.clone().ok_or(
665 ProducerError::InvalidTransactionState(
666 "no txn coordinator cached — did init_transactions succeed?",
667 ),
668 )?;
669 let (producer_id, producer_epoch) = *self.txn_pid_epoch.lock().await;
670 let topic = AddPartitionsToTxnTopic {
671 name: topic.to_owned(),
672 partitions: vec![partition],
673 ..Default::default()
674 };
675 let request = ClientAddPartitionsToTxn(AddPartitionsToTxnRequest {
676 v3_and_below_transactional_id: transactional_id.clone(),
677 v3_and_below_producer_id: producer_id,
678 v3_and_below_producer_epoch: producer_epoch,
679 v3_and_below_topics: vec![topic],
680 ..Default::default()
681 });
682 let mut retry = self.coordinator_retry();
686 loop {
687 let (attempt, last_error) = match coordinator.send(request.clone()).await {
688 Ok(response) => {
689 let code = add_partitions_error_code(&response);
690 (
691 CoordinatorAttempt::Answered(code),
692 ProducerError::Server(code),
693 )
694 }
695 Err(error) if error.is_authentication_failure() => {
699 return Err(ProducerError::Client(error));
700 }
701 Err(error) => (CoordinatorAttempt::Lost, ProducerError::Client(error)),
702 };
703 let rediscover = match txn_retry::decide_add_partitions(attempt) {
704 AddPartitionsDecision::Added => return Ok(()),
705 AddPartitionsDecision::Fenced => return Err(self.fatal_error(FatalError::Fenced)),
706 AddPartitionsDecision::Abortable(code) => {
707 return Err(self.abortable_error(code));
708 }
709 AddPartitionsDecision::Fatal(code) => {
710 return Err(self.fatal_error(FatalError::Server(code)));
711 }
712 AddPartitionsDecision::Retry { rediscover } => rediscover,
713 };
714 if !retry.wait().await {
715 return Err(last_error);
716 }
717 if rediscover {
718 coordinator = match self
719 .rediscovered_txn_coordinator(transactional_id, coordinator, retry.deadline)
720 .await
721 {
722 Ok(coordinator) => coordinator,
723 Err(LookupRefusal::Abortable(code)) => return Err(self.abortable_error(code)),
725 Err(LookupRefusal::Fatal(code)) => {
726 return Err(self.fatal_error(FatalError::Server(code)));
727 }
728 };
729 }
730 }
731 }
732
733 #[must_use]
734 pub fn producer_id(&self) -> i64 {
735 self.identity.id.load(Ordering::Acquire)
736 }
737
738 #[must_use]
739 pub fn producer_epoch(&self) -> i16 {
740 self.identity.epoch.load(Ordering::Acquire)
741 }
742
743 pub async fn transactional_identity(&self) -> Option<(i64, i16)> {
758 minted_identity(*self.txn_pid_epoch.lock().await)
759 }
760
761 #[tracing::instrument(
780 level = "info",
781 skip_all,
782 fields(transactional_id = self.transactional_id.as_deref()),
783 err(level = "info"),
784 )]
785 pub async fn begin_transaction(&self) -> Result<Transaction<'_>, ProducerError> {
786 let guard_generation = self.begin_transaction_state().await?;
787 Ok(Transaction {
788 producer: self,
789 finished: false,
790 guard_generation,
791 })
792 }
793
794 #[tracing::instrument(
809 level = "info",
810 skip_all,
811 fields(transactional_id = self.transactional_id.as_deref()),
812 err(level = "info"),
813 )]
814 pub async fn begin_transaction_owned(
815 self: Arc<Self>,
816 ) -> Result<OwnedTransaction, ProducerError> {
817 let guard_generation = self.begin_transaction_state().await?;
818 Ok(OwnedTransaction {
819 producer: self,
820 finished: false,
821 guard_generation,
822 })
823 }
824
825 async fn begin_transaction_state(&self) -> Result<u64, ProducerError> {
826 if self.transactional_id.is_none() {
827 return Err(ProducerError::NotTransactional);
828 }
829 if let Some(error) = self.fatal_error_state() {
830 return Err(error);
831 }
832 if self.transaction_recovery_required() {
833 return Err(ProducerError::RecoveryRequired);
834 }
835 self.begin_transaction_locked(self.txn_state.lock().await)
836 }
837
838 fn begin_transaction_locked(
841 &self,
842 mut state: MutexGuard<'_, TxnState>,
843 ) -> Result<u64, ProducerError> {
844 match *state {
845 TxnState::Ready => {
846 *state = TxnState::InTransaction;
847 Ok(self
848 .txn_guard_generation
849 .fetch_add(1, Ordering::AcqRel)
850 .wrapping_add(1))
851 }
852 _ => Err(ProducerError::InvalidTransactionState(
853 "begin_transaction must be called after init_transactions and not while another txn is in flight",
854 )),
855 }
856 }
857
858 pub async fn prepare_transaction(&self) -> Result<PreparedTransactionState, ProducerError> {
875 if self.transactional_id.is_none() {
876 return Err(ProducerError::NotTransactional);
877 }
878 if !self.two_phase_commit_enabled {
879 return Err(ProducerError::InvalidTransactionState(
880 "prepare_transaction requires transaction_two_phase_commit_enable=true",
881 ));
882 }
883 if let Some(error) = self.fatal_error_state() {
884 return Err(error);
885 }
886 if self.transaction_recovery_required() {
887 return Err(ProducerError::RecoveryRequired);
888 }
889
890 {
891 let mut state = self.txn_state.lock().await;
892 if *state != TxnState::InTransaction {
893 return Err(ProducerError::InvalidTransactionState(
894 "prepare_transaction must follow begin_transaction",
895 ));
896 }
897 *state = TxnState::Preparing;
898 }
899
900 if let Err(error) = self.flush().await {
901 *self.txn_state.lock().await = TxnState::InTransaction;
902 return Err(error);
903 }
904 if let Some(error) = self.transaction_error_state() {
909 *self.txn_state.lock().await = TxnState::InTransaction;
910 return Err(error);
911 }
912
913 let (producer_id, producer_epoch) = *self.txn_pid_epoch.lock().await;
914 let Ok(prepared) = PreparedTransactionState::new(producer_id, producer_epoch) else {
915 *self.txn_state.lock().await = TxnState::InTransaction;
916 return Err(ProducerError::InvalidTransactionState(
917 "transaction coordinator returned an invalid producer identity",
918 ));
919 };
920 *self.prepared_transaction_state.lock().await = Some(prepared);
921 *self.txn_state.lock().await = TxnState::Prepared;
922 self.resolve_transaction_guard();
923 Ok(prepared)
924 }
925
926 pub async fn complete_transaction(
942 &self,
943 prepared: PreparedTransactionState,
944 ) -> Result<(), ProducerError> {
945 if let Some(error) = self.fatal_error_state() {
946 return Err(error);
947 }
948 if *self.txn_state.lock().await != TxnState::Prepared {
949 return Err(ProducerError::InvalidTransactionState(
950 "complete_transaction requires a prepared transaction",
951 ));
952 }
953 let current = *self.prepared_transaction_state.lock().await;
954 self.end_transaction(current == Some(prepared)).await
955 }
956
957 #[tracing::instrument(
998 level = "info",
999 skip_all,
1000 fields(committed, error_code = field::Empty),
1001 err(level = "info"),
1002 )]
1003 pub(crate) async fn end_transaction(&self, committed: bool) -> Result<(), ProducerError> {
1004 let tid = self
1005 .transactional_id
1006 .clone()
1007 .ok_or(ProducerError::NotTransactional)?;
1008
1009 if let Some(error) = self.fatal_error_state() {
1012 return Err(error);
1013 }
1014
1015 let mut state = self.txn_state.lock().await;
1021 let previous_state = *state;
1022 if !matches!(previous_state, TxnState::InTransaction | TxnState::Prepared) {
1023 return Err(ProducerError::InvalidTransactionState(
1024 "commit/abort_transaction must follow begin_transaction",
1025 ));
1026 }
1027 *state = TxnState::CommittingOrAborting;
1028 drop(state);
1029
1030 if committed {
1031 if let Err(error) = self.flush().await {
1033 *self.txn_state.lock().await = previous_state;
1034 return Err(error);
1035 }
1036
1037 if let Some(error) = self.abortable_error_state() {
1040 *self.txn_state.lock().await = previous_state;
1041 return Err(error);
1042 }
1043 } else {
1044 if let Err(error) = self.abort_undrained_batches().await {
1052 *self.txn_state.lock().await = previous_state;
1053 return Err(error);
1054 }
1055 }
1056
1057 let coord_guard = self.txn_coord_client.lock().await;
1059 let Some(coord) = coord_guard.as_ref().cloned() else {
1060 drop(coord_guard);
1061 *self.txn_state.lock().await = previous_state;
1062 return Err(ProducerError::InvalidTransactionState(
1063 "no txn coordinator cached — did init_transactions succeed?",
1064 ));
1065 };
1066 drop(coord_guard);
1067
1068 let (pid, epoch) = *self.txn_pid_epoch.lock().await;
1069
1070 let request = EndTxnRequest {
1073 transactional_id: tid,
1074 producer_id: pid,
1075 producer_epoch: epoch,
1076 committed,
1077 ..Default::default()
1078 };
1079 let end_txn = self
1080 .send_end_txn_until_decided(coord, request, committed)
1081 .await;
1082
1083 self.apply_end_txn(self.txn_state.lock().await, end_txn, previous_state)
1084 .await
1085 }
1086
1087 async fn apply_end_txn(
1091 &self,
1092 mut state: MutexGuard<'_, TxnState>,
1093 end_txn: Result<(EndTxnDecision, Option<(i64, i16)>), ClientError>,
1094 previous_state: TxnState,
1095 ) -> Result<(), ProducerError> {
1096 let (decision, producer_identity) = match end_txn {
1097 Ok(answer) => answer,
1098 Err(error) => {
1099 self.require_transaction_recovery();
1103 *state = TxnState::RecoveryRequired;
1104 return Err(ProducerError::Client(error));
1105 }
1106 };
1107 match decision {
1108 EndTxnDecision::Complete => {
1109 if let Some(identity) = producer_identity {
1117 self.adopt_transactional_identity(identity).await;
1118 }
1119 *self.prepared_transaction_state.lock().await = None;
1120 self.txn_error.clear_abortable();
1121 *state = TxnState::Ready;
1122 self.resolve_transaction_guard();
1123 Ok(())
1124 }
1125 EndTxnDecision::Fenced => {
1126 *state = TxnState::Fenced;
1127 drop(state);
1128 Err(self.fatal_error(FatalError::Fenced))
1129 }
1130 EndTxnDecision::Fatal(code) => {
1131 *state = TxnState::FatalError;
1132 drop(state);
1133 Err(self.fatal_error(FatalError::Server(code)))
1134 }
1135 EndTxnDecision::ConcurrentTransactions => {
1136 *state = previous_state; Err(ProducerError::ConcurrentTransactions)
1138 }
1139 EndTxnDecision::Abortable(code) => {
1140 *state = previous_state;
1141 drop(state);
1142 Err(self.abortable_error(code))
1143 }
1144 EndTxnDecision::TimedOut(code) => {
1145 *state = previous_state;
1146 Err(ProducerError::Server(code))
1147 }
1148 EndTxnDecision::OutcomeUnknown | EndTxnDecision::Retry { .. } => {
1149 self.require_transaction_recovery();
1152 *state = TxnState::RecoveryRequired;
1153 Err(ProducerError::RecoveryRequired)
1154 }
1155 }
1156 }
1157
1158 async fn adopt_transactional_identity(&self, identity: (i64, i16)) {
1169 let mut current = self.txn_pid_epoch.lock().await;
1170 if *current != identity {
1171 self.next_seq.clear();
1172 }
1173 *current = identity;
1174 }
1175
1176 async fn send_end_txn_until_decided(
1195 &self,
1196 mut coordinator: Client,
1197 request: EndTxnRequest,
1198 committed: bool,
1199 ) -> Result<(EndTxnDecision, Option<(i64, i16)>), ClientError> {
1200 let deadline = Instant::now() + self.init_retry_timeout.to_std();
1201 let max_backoff = self.retry_backoff_max.to_std();
1202 let mut backoff = self.init_retry_backoff.to_std();
1203 let mut earlier_attempt_lost = false;
1204 loop {
1205 let sent = send_in_transaction_version::<_, END_TXN_TRANSACTION_V1_MAX_VERSION>(
1206 &coordinator,
1207 request.clone(),
1208 self.transaction_v2(),
1209 )
1210 .await;
1211 let (attempt, identity) = match sent {
1212 Ok(response) => {
1213 tracing::Span::current().record("error_code", response.error_code);
1214 let identity = (response.producer_id >= 0)
1215 .then_some((response.producer_id, response.producer_epoch));
1216 (CoordinatorAttempt::Answered(response.error_code), identity)
1217 }
1218 Err(error) if error.is_authentication_failure() => return Err(error),
1219 Err(error) => {
1220 tracing::warn!(
1221 %error,
1222 committed = request.committed,
1223 "EndTxn request lost in transport; retrying to learn the outcome"
1224 );
1225 (CoordinatorAttempt::Lost, None)
1226 }
1227 };
1228 let decision = txn_retry::decide_end_txn(attempt, earlier_attempt_lost, committed);
1229 earlier_attempt_lost |= attempt == CoordinatorAttempt::Lost;
1230 let EndTxnDecision::Retry { rediscover } = decision else {
1231 return Ok((decision, identity));
1232 };
1233 let remaining = deadline.saturating_duration_since(Instant::now());
1234 if remaining.is_zero() {
1235 return Ok((
1236 txn_retry::decide_end_txn_at_deadline(attempt, earlier_attempt_lost),
1237 None,
1238 ));
1239 }
1240 time::sleep(backoff.min(remaining)).await;
1241 backoff = backoff.saturating_mul(2).min(max_backoff);
1242 if rediscover {
1243 match self
1244 .reconnect_txn_coordinator(&request.transactional_id, deadline)
1245 .await
1246 {
1247 Ok(fresh) => coordinator = fresh,
1248 Err(LookupError::Refused(refusal)) => {
1249 let decision = txn_retry::decide_end_txn_after_refused_lookup(
1250 refusal,
1251 earlier_attempt_lost,
1252 committed,
1253 );
1254 if !matches!(decision, EndTxnDecision::Retry { .. }) {
1255 return Ok((decision, None));
1256 }
1257 }
1258 Err(LookupError::Unresolved(error)) => {
1259 tracing::warn!(%error, "transaction coordinator lookup failed; retrying");
1260 coordinator.reconnect_bootstrap().await;
1263 }
1264 }
1265 }
1266 }
1267 }
1268
1269 async fn reconnect_txn_coordinator(
1272 &self,
1273 tid: &str,
1274 deadline: Instant,
1275 ) -> Result<Client, LookupError> {
1276 let address = self.find_txn_coordinator(tid, deadline).await?;
1277 let coordinator = self.connect_txn_coordinator(address).await?;
1278 *self.txn_coord_client.lock().await = Some(coordinator.clone());
1279 Ok(coordinator)
1280 }
1281
1282 async fn connect_txn_coordinator(&self, address: String) -> Result<Client, ProducerError> {
1284 Ok(Client::builder()
1285 .bootstrap(address)
1286 .client_id(self.client_id.clone())
1287 .maybe_security(self.security.clone())
1288 .dns_timeout(self.dns_timeout)
1289 .metadata_recovery_strategy(self.metadata_recovery_strategy)
1290 .metadata_recovery_rebootstrap_trigger(
1291 self.metadata_recovery_rebootstrap_trigger.time(),
1292 )
1293 .client_dns_lookup(self.client_dns_lookup)
1294 .reconnect_backoff(self.reconnect_backoff)
1295 .reconnect_backoff_max(self.reconnect_backoff_max)
1296 .connections_max_idle(self.connections_max_idle)
1297 .socket_connection_setup_timeout(self.socket_connection_setup_timeout)
1298 .socket_connection_setup_timeout_max(self.socket_connection_setup_timeout_max)
1299 .dispatch_queue_capacity(self.dispatch_queue_capacity.get())
1300 .frame_max(self.frame_max.size())
1301 .send_buffer(self.socket_buffers.send)
1302 .receive_buffer(self.socket_buffers.receive)
1303 .request_timeout(self.request_timeout)
1304 .build()
1305 .await?)
1306 }
1307
1308 #[tracing::instrument(
1345 level = "info",
1346 skip_all,
1347 fields(
1348 transactional_id = self.transactional_id.as_deref(),
1349 coordinator = field::Empty,
1350 producer_id = field::Empty,
1351 producer_epoch = field::Empty,
1352 error_code = field::Empty,
1353 ),
1354 err(level = "info"),
1355 )]
1356 pub async fn init_transactions(&self) -> Result<(), ProducerError> {
1357 self.init_transactions_with_keep_prepared(false).await
1358 }
1359
1360 #[tracing::instrument(
1374 level = "info",
1375 skip_all,
1376 fields(
1377 transactional_id = self.transactional_id.as_deref(),
1378 keep_prepared,
1379 coordinator = field::Empty,
1380 producer_id = field::Empty,
1381 producer_epoch = field::Empty,
1382 error_code = field::Empty,
1383 ),
1384 err(level = "info"),
1385 )]
1386 pub async fn init_transactions_with_keep_prepared(
1387 &self,
1388 keep_prepared: bool,
1389 ) -> Result<(), ProducerError> {
1390 let Some(tid) = self.transactional_id.as_deref() else {
1391 return Err(ProducerError::NotTransactional);
1392 };
1393 if let Some(error) = self.fatal_error_state() {
1397 return Err(error);
1398 }
1399
1400 let previous_state = {
1401 let mut state = self.txn_state.lock().await;
1402 if !matches!(
1403 *state,
1404 TxnState::Uninitialized | TxnState::Ready | TxnState::RecoveryRequired
1405 ) && !self.transaction_recovery_required()
1406 {
1407 return Err(ProducerError::InvalidTransactionState(
1408 "init_transactions called while a transaction is in flight",
1409 ));
1410 }
1411 let previous_state = *state;
1412 *state = TxnState::Initializing;
1413 previous_state
1414 };
1415
1416 let deadline = Instant::now() + self.init_retry_timeout.to_std();
1419 let initialized = async {
1420 let coord_addr = match self.find_txn_coordinator(tid, deadline).await {
1421 Ok(address) => address,
1422 Err(error) => return Err(self.init_lookup_error(error).await),
1423 };
1424 tracing::Span::current().record("coordinator", coord_addr.as_str());
1425
1426 let coord = self.connect_txn_coordinator(coord_addr).await?;
1427
1428 self.init_producer_id_on_coordinator(
1429 tid,
1430 &InitProducerIdRequest {
1431 transactional_id: Some(tid.to_owned()),
1432 transaction_timeout_ms: self.transaction_timeout_ms,
1433 enable2_pc: self.two_phase_commit_enabled,
1434 keep_prepared_txn: keep_prepared,
1435 ..Default::default()
1436 },
1437 coord,
1438 deadline,
1439 )
1440 .await
1441 }
1442 .await;
1443 let (coord, resp) = match initialized {
1444 Ok(initialized) => initialized,
1445 Err(error) => {
1446 if self.fatal_error_state().is_none() {
1448 *self.txn_state.lock().await = previous_state;
1449 }
1450 return Err(error);
1451 }
1452 };
1453
1454 tracing::Span::current().record("error_code", resp.error_code);
1455 match resp.error_code {
1456 0 => {
1457 let recovered = if keep_prepared && resp.ongoing_txn_producer_id >= 0 {
1458 let Ok(recovered) = PreparedTransactionState::new(
1459 resp.ongoing_txn_producer_id,
1460 resp.ongoing_txn_producer_epoch,
1461 ) else {
1462 *self.txn_state.lock().await = previous_state;
1463 return Err(ProducerError::InvalidTransactionState(
1464 "transaction coordinator returned an invalid ongoing identity",
1465 ));
1466 };
1467 Some(recovered)
1468 } else {
1469 None
1470 };
1471 tracing::Span::current().record("producer_id", resp.producer_id);
1472 tracing::Span::current().record("producer_epoch", resp.producer_epoch);
1473 self.adopt_transactional_identity((resp.producer_id, resp.producer_epoch))
1474 .await;
1475 self.txn_error.clear_abortable();
1476 self.transaction_v2.store(
1480 transaction_v2_enabled(&coord.finalized_features()),
1481 Ordering::Release,
1482 );
1483 *self.txn_coord_client.lock().await = Some(coord);
1484 *self.prepared_transaction_state.lock().await = recovered;
1485 *self.txn_state.lock().await = if recovered.is_some() {
1486 TxnState::Prepared
1487 } else {
1488 TxnState::Ready
1489 };
1490 self.txn_recovery_required.store(false, Ordering::Release);
1491 self.resolve_transaction_guard();
1492 Ok(())
1493 }
1494 code => match txn_retry::decide_init_producer_id(CoordinatorAttempt::Answered(code)) {
1495 TxnRequestDecision::Fenced => Err(self.fence_transaction().await),
1496 TxnRequestDecision::Fatal(code) => Err(self.fatal_transaction(code).await),
1497 TxnRequestDecision::Done
1500 | TxnRequestDecision::Abortable(_)
1501 | TxnRequestDecision::Retry { .. } => {
1502 *self.txn_state.lock().await = previous_state;
1503 Err(ProducerError::Server(code))
1504 }
1505 },
1506 }
1507 }
1508
1509 async fn init_producer_id_on_coordinator(
1519 &self,
1520 transactional_id: &str,
1521 request: &InitProducerIdRequest,
1522 coordinator: Client,
1523 deadline: Instant,
1524 ) -> Result<(Client, InitProducerIdResponse), ProducerError> {
1525 let mut coordinator = coordinator;
1526 let mut retry = self.coordinator_retry_until(deadline);
1527 loop {
1528 let (attempt, last_outcome) = match send_init_producer_id(&coordinator, request).await {
1529 Ok(response) => (
1530 CoordinatorAttempt::Answered(response.error_code),
1531 Ok(response),
1532 ),
1533 Err(error @ ClientError::Disconnected) => (CoordinatorAttempt::Lost, Err(error)),
1534 Err(error) => return Err(ProducerError::Client(error)),
1535 };
1536 let TxnRequestDecision::Retry { rediscover } =
1537 txn_retry::decide_init_producer_id(attempt)
1538 else {
1539 return last_outcome
1540 .map(|response| (coordinator, response))
1541 .map_err(ProducerError::Client);
1542 };
1543 if !retry.wait().await {
1544 return last_outcome
1545 .map(|response| (coordinator, response))
1546 .map_err(ProducerError::Client);
1547 }
1548 if rediscover {
1549 coordinator = match self
1550 .rediscovered_txn_coordinator(transactional_id, coordinator, deadline)
1551 .await
1552 {
1553 Ok(coordinator) => coordinator,
1554 Err(refusal) => {
1555 return Err(self.init_lookup_error(LookupError::Refused(refusal)).await);
1556 }
1557 };
1558 }
1559 }
1560 }
1561
1562 #[tracing::instrument(level = "debug", skip_all, fields(transactional_id = %tid), err(level = "debug"))]
1565 async fn find_txn_coordinator(
1566 &self,
1567 tid: &str,
1568 deadline: Instant,
1569 ) -> Result<String, LookupError> {
1570 self.find_coordinator(tid, CoordinatorKeyType::Transaction, deadline)
1571 .await
1572 }
1573
1574 async fn find_coordinator(
1584 &self,
1585 key: &str,
1586 key_type: CoordinatorKeyType,
1587 deadline: Instant,
1588 ) -> Result<String, LookupError> {
1589 let mut retry = self.coordinator_retry_until(deadline);
1590 loop {
1591 let (attempt, address, last_error) = match self
1592 .client
1593 .send(build_find_coordinator(key, key_type))
1594 .await
1595 {
1596 Ok(response) => {
1597 let (code, address) = coordinator_answer(&response);
1598 (
1599 CoordinatorAttempt::Answered(code),
1600 address,
1601 ProducerError::Server(code),
1602 )
1603 }
1604 Err(error) if lookup_may_succeed_later(&error) => (
1605 CoordinatorAttempt::Lost,
1606 String::new(),
1607 ProducerError::Client(error),
1608 ),
1609 Err(error) => {
1610 return Err(LookupError::Unresolved(ProducerError::Client(error)));
1611 }
1612 };
1613 match txn_retry::decide_find_coordinator(attempt) {
1614 FindCoordinatorDecision::Found => return Ok(address),
1615 FindCoordinatorDecision::Retry => {}
1616 FindCoordinatorDecision::Refused(refusal) => {
1617 tracing::warn!(
1618 ?key_type,
1619 key,
1620 ?refusal,
1621 "could not find a coordinator; the broker refused the lookup"
1622 );
1623 return Err(LookupError::Refused(refusal));
1624 }
1625 }
1626 if attempt == CoordinatorAttempt::Lost {
1627 self.client.reconnect_bootstrap().await;
1629 }
1630 if !retry.wait().await {
1631 return Err(LookupError::Unresolved(last_error));
1632 }
1633 }
1634 }
1635
1636 async fn refused_lookup(&self, refusal: LookupRefusal) -> ProducerError {
1642 match refusal {
1643 LookupRefusal::Abortable(code) => self.abortable_error(code),
1644 LookupRefusal::Fatal(code) => self.fatal_transaction(code).await,
1645 }
1646 }
1647
1648 async fn init_lookup_error(&self, error: LookupError) -> ProducerError {
1654 match error {
1655 LookupError::Refused(LookupRefusal::Fatal(code)) => self.fatal_transaction(code).await,
1656 LookupError::Refused(LookupRefusal::Abortable(code)) => ProducerError::Server(code),
1657 LookupError::Unresolved(error) => error,
1658 }
1659 }
1660
1661 #[tracing::instrument(
1699 level = "info",
1700 skip_all,
1701 fields(
1702 transactional_id = self.transactional_id.as_deref(),
1703 group_id = %group_meta.group_id,
1704 generation_id = group_meta.generation_id,
1705 offset_count = field::Empty,
1706 ),
1707 err(level = "info"),
1708 )]
1709 pub async fn send_offsets_to_transaction(
1710 &self,
1711 offsets: impl IntoIterator<Item = ((String, i32), i64)>,
1712 group_meta: &ConsumerGroupMetadata,
1713 ) -> Result<(), ProducerError> {
1714 let tid = self
1715 .transactional_id
1716 .as_deref()
1717 .ok_or(ProducerError::NotTransactional)?
1718 .to_owned();
1719 if let Some(error) = self.fatal_error_state() {
1720 return Err(error);
1721 }
1722 let state = *self.txn_state.lock().await;
1723 match state {
1724 TxnState::InTransaction => {}
1725 TxnState::Preparing | TxnState::Prepared => {
1726 return Err(ProducerError::InvalidTransactionState(
1727 "send_offsets_to_transaction is not allowed after prepare_transaction",
1728 ));
1729 }
1730 _ => {
1731 return Err(ProducerError::InvalidTransactionState(
1732 "cannot send offsets to transaction while no transaction is in progress",
1733 ));
1734 }
1735 }
1736 let offsets_vec: Vec<_> = offsets.into_iter().collect();
1737 tracing::Span::current().record("offset_count", offsets_vec.len());
1738 if let Some(error) = self.abortable_error_state() {
1739 return Err(error);
1740 }
1741
1742 let topic_ids = self.offset_topic_ids(&offsets_vec).await?;
1746 let (pid, epoch) = *self.txn_pid_epoch.lock().await;
1747
1748 if !self.transaction_v2() {
1753 self.add_offsets_to_txn(&tid, pid, epoch, &group_meta.group_id)
1754 .await?;
1755 }
1756
1757 let topics = build_topics_payload(&offsets_vec, &topic_ids);
1763 self.txn_offset_commit(&tid, pid, epoch, group_meta, topics)
1764 .await
1765 }
1766
1767 async fn offset_topic_ids(
1771 &self,
1772 offsets: &[((String, i32), i64)],
1773 ) -> Result<HashMap<String, Uuid>, ProducerError> {
1774 let mut topics: Vec<String> = Vec::new();
1775 for ((topic, _), _) in offsets {
1776 if !topics.contains(topic) {
1777 topics.push(topic.clone());
1778 }
1779 }
1780 for topic in &topics {
1783 self.client.metadata_topics().add(topic);
1784 }
1785 self.metadata_wait()
1786 .topic_ids(&topics, |topics| self.refresh_producer_metadata(topics))
1787 .await
1788 }
1789
1790 async fn add_offsets_to_txn(
1799 &self,
1800 transactional_id: &str,
1801 producer_id: i64,
1802 producer_epoch: i16,
1803 group_id: &str,
1804 ) -> Result<(), ProducerError> {
1805 let mut coordinator = self.txn_coord_client.lock().await.clone().ok_or(
1806 ProducerError::InvalidTransactionState(
1807 "no txn coordinator cached — did init_transactions succeed?",
1808 ),
1809 )?;
1810 let request = AddOffsetsToTxnRequest {
1811 transactional_id: transactional_id.to_owned(),
1812 producer_id,
1813 producer_epoch,
1814 group_id: group_id.to_owned(),
1815 ..Default::default()
1816 };
1817 let mut retry = self.coordinator_retry();
1818 loop {
1819 let (attempt, last_error) = match coordinator.send(request.clone()).await {
1820 Ok(response) => (
1821 CoordinatorAttempt::Answered(response.error_code),
1822 ProducerError::Server(response.error_code),
1823 ),
1824 Err(error) if request_may_have_reached_the_broker(&error) => {
1825 (CoordinatorAttempt::Lost, ProducerError::Client(error))
1826 }
1827 Err(error) => return Err(ProducerError::Client(error)),
1828 };
1829 let rediscover = match txn_retry::decide_add_offsets_to_txn(attempt) {
1830 TxnRequestDecision::Done => return Ok(()),
1831 TxnRequestDecision::Fenced => return Err(self.fence_transaction().await),
1832 TxnRequestDecision::Abortable(code) => return Err(self.abortable_error(code)),
1833 TxnRequestDecision::Fatal(code) => return Err(self.fatal_transaction(code).await),
1834 TxnRequestDecision::Retry { rediscover } => rediscover,
1835 };
1836 if !retry.wait().await {
1837 return Err(last_error);
1838 }
1839 if rediscover {
1840 coordinator = match self
1841 .rediscovered_txn_coordinator(transactional_id, coordinator, retry.deadline)
1842 .await
1843 {
1844 Ok(coordinator) => coordinator,
1845 Err(refusal) => return Err(self.refused_lookup(refusal).await),
1846 };
1847 }
1848 }
1849 }
1850
1851 async fn txn_offset_commit(
1867 &self,
1868 transactional_id: &str,
1869 producer_id: i64,
1870 producer_epoch: i16,
1871 group_meta: &ConsumerGroupMetadata,
1872 topics: Vec<TxnOffsetCommitRequestTopic>,
1873 ) -> Result<(), ProducerError> {
1874 let cap = TxnOffsetCommitCap::of(&topics, self.transaction_v2());
1875 let request = TxnOffsetCommitRequest {
1876 transactional_id: transactional_id.to_owned(),
1877 producer_id,
1878 producer_epoch,
1879 group_id: group_meta.group_id.clone(),
1880 generation_id_or_member_epoch: group_meta.generation_id,
1881 member_id: group_meta.member_id.clone(),
1882 group_instance_id: group_meta.group_instance_id.clone(),
1883 topics,
1884 ..Default::default()
1885 };
1886 let mut retry = self.coordinator_retry();
1887 let mut group_client = match self
1888 .connect_group_coordinator(&group_meta.group_id, retry.deadline)
1889 .await
1890 {
1891 Ok(client) => client,
1892 Err(LookupError::Refused(refusal)) => return Err(self.refused_lookup(refusal).await),
1893 Err(LookupError::Unresolved(error)) => return Err(error),
1894 };
1895 loop {
1896 let (attempt, last_error) = match cap.send(&group_client, request.clone()).await {
1897 Ok(response) => {
1898 let code = txn_offset_commit_error_code(&response);
1899 (
1900 CoordinatorAttempt::Answered(code),
1901 ProducerError::Server(code),
1902 )
1903 }
1904 Err(error) if request_may_have_reached_the_broker(&error) => {
1905 (CoordinatorAttempt::Lost, ProducerError::Client(error))
1906 }
1907 Err(error) => return Err(ProducerError::Client(error)),
1908 };
1909 let rediscover = match txn_retry::decide_txn_offset_commit(attempt) {
1910 TxnRequestDecision::Done => return Ok(()),
1911 TxnRequestDecision::Fenced => return Err(self.fence_transaction().await),
1912 TxnRequestDecision::Abortable(code) => return Err(self.abortable_error(code)),
1913 TxnRequestDecision::Fatal(code) => return Err(self.fatal_transaction(code).await),
1914 TxnRequestDecision::Retry { rediscover } => rediscover,
1915 };
1916 if !retry.wait().await {
1917 return Err(last_error);
1918 }
1919 if rediscover {
1920 match self
1921 .connect_group_coordinator(&group_meta.group_id, retry.deadline)
1922 .await
1923 {
1924 Ok(fresh) => group_client = fresh,
1925 Err(LookupError::Refused(refusal)) => {
1926 return Err(self.refused_lookup(refusal).await);
1927 }
1928 Err(LookupError::Unresolved(error)) => {
1929 tracing::warn!(%error, "group coordinator lookup failed; retrying");
1930 self.client.reconnect_bootstrap().await;
1933 }
1934 }
1935 }
1936 }
1937 }
1938
1939 async fn connect_group_coordinator(
1942 &self,
1943 group_id: &str,
1944 deadline: Instant,
1945 ) -> Result<Client, LookupError> {
1946 let group_addr = self.find_group_coordinator(group_id, deadline).await?;
1947 Client::builder()
1948 .bootstrap(group_addr)
1949 .client_id(self.client_id.clone())
1950 .maybe_security(self.security.clone())
1951 .dns_timeout(self.dns_timeout)
1952 .metadata_recovery_strategy(self.metadata_recovery_strategy)
1953 .metadata_recovery_rebootstrap_trigger(
1954 self.metadata_recovery_rebootstrap_trigger.time(),
1955 )
1956 .client_dns_lookup(self.client_dns_lookup)
1957 .reconnect_backoff(self.reconnect_backoff)
1958 .reconnect_backoff_max(self.reconnect_backoff_max)
1959 .connections_max_idle(self.connections_max_idle)
1960 .socket_connection_setup_timeout(self.socket_connection_setup_timeout)
1961 .socket_connection_setup_timeout_max(self.socket_connection_setup_timeout_max)
1962 .dispatch_queue_capacity(self.dispatch_queue_capacity.get())
1963 .frame_max(self.frame_max.size())
1964 .request_timeout(self.request_timeout)
1965 .send_buffer(self.socket_buffers.send)
1966 .receive_buffer(self.socket_buffers.receive)
1967 .build()
1968 .await
1969 .map_err(|error| LookupError::Unresolved(error.into()))
1970 }
1971
1972 #[tracing::instrument(level = "debug", skip_all, fields(group_id = %group_id), err(level = "debug"))]
1980 async fn find_group_coordinator(
1981 &self,
1982 group_id: &str,
1983 deadline: Instant,
1984 ) -> Result<String, LookupError> {
1985 self.find_coordinator(group_id, CoordinatorKeyType::Group, deadline)
1986 .await
1987 }
1988
1989 fn coordinator_retry(&self) -> CoordinatorRetry {
1994 self.coordinator_retry_until(Instant::now() + self.init_retry_timeout.to_std())
1995 }
1996
1997 fn coordinator_retry_until(&self, deadline: Instant) -> CoordinatorRetry {
2000 CoordinatorRetry {
2001 deadline,
2002 backoff: self.init_retry_backoff.to_std(),
2003 max_backoff: self.retry_backoff_max.to_std(),
2004 }
2005 }
2006
2007 async fn rediscovered_txn_coordinator(
2012 &self,
2013 transactional_id: &str,
2014 current: Client,
2015 deadline: Instant,
2016 ) -> Result<Client, LookupRefusal> {
2017 match self
2018 .reconnect_txn_coordinator(transactional_id, deadline)
2019 .await
2020 {
2021 Ok(fresh) => Ok(fresh),
2022 Err(LookupError::Refused(refusal)) => Err(refusal),
2023 Err(LookupError::Unresolved(error)) => {
2024 tracing::warn!(%error, "transaction coordinator lookup failed; retrying");
2025 current.reconnect_bootstrap().await;
2026 Ok(current)
2027 }
2028 }
2029 }
2030
2031 async fn fence_transaction(&self) -> ProducerError {
2036 *self.txn_state.lock().await = TxnState::Fenced;
2037 self.fatal_error(FatalError::Fenced)
2038 }
2039
2040 async fn fatal_transaction(&self, code: i16) -> ProducerError {
2043 *self.txn_state.lock().await = TxnState::FatalError;
2044 self.fatal_error(FatalError::Server(code))
2045 }
2046
2047 fn fatal_error(&self, error: FatalError) -> ProducerError {
2056 self.txn_error.set_fatal(error);
2057 self.resolve_transaction_guard();
2058 tracing::error!(
2059 ?error,
2060 "the transactional producer is in a fatal error state; close it"
2061 );
2062 let _ = self.wake_tx.try_send(DrainIntent::Ready);
2063 error.error()
2064 }
2065
2066 fn fatal_error_state(&self) -> Option<ProducerError> {
2069 self.txn_error.fatal().map(FatalError::error)
2070 }
2071
2072 fn transaction_error_state(&self) -> Option<ProducerError> {
2075 self.fatal_error_state()
2076 .or_else(|| self.abortable_error_state())
2077 }
2078
2079 fn abortable_error(&self, code: i16) -> ProducerError {
2083 self.txn_error.set_abortable(code);
2084 tracing::warn!(
2085 error_code = code,
2086 "the transaction can no longer commit; abort it"
2087 );
2088 ProducerError::Server(code)
2089 }
2090
2091 fn abortable_error_state(&self) -> Option<ProducerError> {
2093 self.txn_error.abortable().map(|error| match error {
2094 AbortableError::Server(code) => ProducerError::Server(code),
2095 AbortableError::Timeout => ProducerError::SendTimeout,
2096 })
2097 }
2098
2099 fn transaction_v2(&self) -> bool {
2101 self.transaction_v2.load(Ordering::Acquire)
2102 }
2103
2104 pub(crate) fn is_active(&self) -> Result<(), ProducerError> {
2105 match self.state.load(Ordering::Acquire) {
2106 STATE_ACTIVE => Ok(()),
2107 STATE_FENCED => Err(ProducerError::FencedProducer),
2108 _ => Err(ProducerError::Closed),
2109 }
2110 }
2111
2112 pub(crate) fn require_transaction_recovery(&self) {
2113 if !self.txn_recovery_required.swap(true, Ordering::AcqRel) {
2114 self.txn_recovery_generation.fetch_add(1, Ordering::AcqRel);
2115 }
2116 let _ = self.wake_tx.try_send(DrainIntent::Ready);
2117 }
2118
2119 fn transaction_recovery_required(&self) -> bool {
2120 self.txn_recovery_required.load(Ordering::Acquire)
2121 }
2122
2123 pub(crate) fn abandon_transaction_guard(&self, generation: u64) {
2124 if self
2125 .txn_guard_generation
2126 .compare_exchange(
2127 generation,
2128 generation.wrapping_add(1),
2129 Ordering::AcqRel,
2130 Ordering::Acquire,
2131 )
2132 .is_ok()
2133 {
2134 self.require_transaction_recovery();
2135 }
2136 }
2137
2138 fn resolve_transaction_guard(&self) {
2139 let generation = self.txn_guard_generation.load(Ordering::Acquire);
2140 if generation % 2 == 1 {
2141 let _ = self.txn_guard_generation.compare_exchange(
2142 generation,
2143 generation.wrapping_add(1),
2144 Ordering::AcqRel,
2145 Ordering::Acquire,
2146 );
2147 }
2148 }
2149
2150 #[allow(dead_code)] pub(crate) fn fence(&self) {
2152 let _ = self.state.compare_exchange(
2153 STATE_ACTIVE,
2154 STATE_FENCED,
2155 Ordering::AcqRel,
2156 Ordering::Acquire,
2157 );
2158 }
2159
2160 pub async fn send(&self, record: ProducerRecord) -> Result<RecordMetadata, ProducerError> {
2169 self.enqueue(record).await?.await
2170 }
2171
2172 pub async fn enqueue(&self, record: ProducerRecord) -> Result<DeliveryHandle, ProducerError> {
2193 let span = tracing::debug_span!(
2194 "producer.send",
2195 topic = %record.topic,
2196 partition = field::Empty,
2197 );
2198 self.enqueue_inner(record).instrument(span).await
2199 }
2200
2201 async fn enqueue_inner(&self, record: ProducerRecord) -> Result<DeliveryHandle, ProducerError> {
2202 record.validate()?;
2205 self.is_active()?;
2206 if self.transactional_id.is_some() && self.transaction_recovery_required() {
2207 return Err(ProducerError::RecoveryRequired);
2208 }
2209
2210 if self.transactional_id.is_some() {
2220 let state = *self.txn_state.lock().await;
2221 self.append_transaction_generation(Some(state))?;
2222 }
2223
2224 let metadata_started = Instant::now();
2227 let partition_count = self
2228 .partition_count(&record.topic, record.partition)
2229 .await?;
2230 let fixed_partition = match record.partition {
2234 Some(partition) => Some(partition),
2235 None => match &self.custom_partitioner {
2236 Some(partitioner) => {
2237 let (available, all) = self.partition_infos(&record.topic, partition_count);
2238 let picked = partitioner.partition(
2239 &record.topic,
2240 record.key.as_deref(),
2241 record.value.as_deref(),
2242 &available,
2243 &all,
2244 );
2245 if picked < 0 {
2246 return Err(ProducerError::InvalidPartitionerResult(picked));
2247 }
2248 Some(picked)
2249 }
2250 None => self
2251 .partitioner
2252 .keyed_partition(record.key.as_deref(), partition_count),
2253 },
2254 };
2255 let has_leader = |partition: i32| {
2256 self.partition_leaders
2257 .get(&(record.topic.clone(), partition))
2258 .is_some_and(|leader| *leader >= 0)
2259 };
2260 let partitions = TopicPartitions {
2261 count: partition_count,
2262 has_leader: &has_leader,
2263 };
2264 let waited_on_metadata = Duration::from_millis(
2267 u64::try_from(metadata_started.elapsed().as_millis()).unwrap_or(u64::MAX),
2268 );
2269 let remaining_block = self.max_block.saturating_sub(waited_on_metadata);
2270
2271 let serialized_size = record_size_upper_bound(
2275 record.key.as_deref(),
2276 record.value.as_deref(),
2277 &record.headers,
2278 );
2279 if let Some(limit) = self.record_size_limit(serialized_size) {
2280 return Err(ProducerError::RecordTooLarge {
2281 record_size: serialized_size,
2282 limit,
2283 });
2284 }
2285 let record_size = approx_record_size(
2286 record.key.as_deref(),
2287 record.value.as_deref(),
2288 &record.headers,
2289 );
2290
2291 let timestamp = record.timestamp_ms.unwrap_or_else(current_millis);
2292 let mut memory = None;
2293 loop {
2294 let sticky = fixed_partition
2299 .is_none()
2300 .then(|| self.peek_sticky(&record.topic, &partitions));
2301 let partition = fixed_partition
2302 .or_else(|| sticky.map(StickyPartition::partition))
2303 .unwrap_or_default();
2304 tracing::Span::current().record("partition", partition);
2305 let acc = Arc::clone(
2306 self.accumulators
2307 .entry((record.topic.clone(), partition))
2308 .or_insert_with(|| Arc::new(Mutex::new(Accumulator::new(self.batch_size))))
2309 .value(),
2310 );
2311
2312 if memory.is_none() {
2322 let state = match &self.transactional_id {
2323 Some(_) => Some(*self.txn_state.lock().await),
2324 None => None,
2325 };
2326 let expected_generation = self.append_transaction_generation(state)?;
2327 let needs_memory = {
2328 let mut a = acc.lock().await;
2329 if self.sticky_partition_changed(&record.topic, sticky, &a, &partitions) {
2330 continue;
2331 }
2332 let needs_memory = a.needs_new_batch(record_size, expected_generation);
2333 if needs_memory && a.current.as_ref().is_some_and(|batch| !batch.is_empty()) {
2337 a.seal_current();
2338 drop(a);
2339 let _ = self.wake_tx.try_send(DrainIntent::Ready);
2340 }
2341 needs_memory
2342 };
2343 if needs_memory {
2344 let size = self.batch_size.max(record_size);
2345 memory = Some(self.buffer_pool.allocate(size, remaining_block).await?);
2346 }
2347 }
2348
2349 let mut transaction_state = if self.transactional_id.is_some() {
2354 Some(self.txn_state.lock().await)
2355 } else {
2356 None
2357 };
2358 let transaction_generation =
2359 self.append_transaction_generation(transaction_state.as_deref().copied())?;
2360 if transaction_generation.is_some()
2361 && let Err(error) = self
2362 .register_transaction_partition(&record.topic, partition)
2363 .await
2364 {
2365 if let (Some(state), Some(fatal)) =
2367 (transaction_state.as_deref_mut(), self.txn_error.fatal())
2368 {
2369 *state = fatal.state();
2370 }
2371 return Err(error);
2372 }
2373 let mut a = self
2374 .lock_accumulator_for_append(
2375 &acc,
2376 transaction_state.as_deref().copied(),
2377 transaction_generation,
2378 )
2379 .await?;
2380 if self.sticky_partition_changed(&record.topic, sticky, &a, &partitions) {
2381 continue;
2385 }
2386 if memory.is_none() && a.needs_new_batch(record_size, transaction_generation) {
2387 continue;
2391 }
2392 self.is_active()?;
2393 let AppendResult {
2394 receiver: rx,
2395 wakes_sender,
2396 } = a.append(
2397 record.key,
2398 record.value,
2399 record.headers,
2400 timestamp,
2401 transaction_generation,
2402 memory.take(),
2403 );
2404 drop(memory);
2405 if let Some(sticky) = sticky {
2406 self.update_sticky(
2408 &record.topic,
2409 sticky,
2410 record_size,
2411 &partitions,
2412 a.all_batches_full(),
2413 );
2414 }
2415 drop(a);
2416 drop(acc);
2417 drop(transaction_state);
2418 wake_sender_after_append(&self.wake_tx, self.linger, wakes_sender);
2419 return Ok(DeliveryHandle::new(rx));
2420 }
2421 }
2422
2423 fn sticky_partition_changed(
2430 &self,
2431 topic: &str,
2432 sticky: Option<StickyPartition>,
2433 accumulator: &Accumulator,
2434 partitions: &TopicPartitions<'_>,
2435 ) -> bool {
2436 let Some(sticky) = sticky else {
2437 return false;
2438 };
2439 if self.partitioner.is_changed(topic, sticky) {
2440 return true;
2441 }
2442 if accumulator.all_batches_full() {
2443 self.update_sticky(topic, sticky, 0, partitions, true);
2444 return self.partitioner.is_changed(topic, sticky);
2445 }
2446 false
2447 }
2448
2449 fn leader_rack(&self, topic: &str, partition: i32) -> Option<String> {
2454 let leader = *self.partition_leaders.get(&(topic.to_owned(), partition))?;
2455 if leader < 0 {
2456 return None;
2457 }
2458 let rack = self.broker_racks.get(&leader)?;
2459 rack.clone()
2460 }
2461
2462 fn peek_sticky(&self, topic: &str, partitions: &TopicPartitions<'_>) -> StickyPartition {
2465 match self.client_rack.as_deref() {
2466 Some(client_rack) if self.partitioner.config().rack_aware => {
2467 let leader_rack = |partition: i32| self.leader_rack(topic, partition);
2468 let rack = RackPreference {
2469 leader_rack: &leader_rack,
2470 client_rack,
2471 };
2472 self.partitioner.peek_rack_aware(topic, partitions, rack)
2473 }
2474 _ => self.partitioner.peek(topic, partitions),
2475 }
2476 }
2477
2478 fn update_sticky(
2481 &self,
2482 topic: &str,
2483 sticky: StickyPartition,
2484 appended: usize,
2485 partitions: &TopicPartitions<'_>,
2486 enable_switch: bool,
2487 ) {
2488 match self.client_rack.as_deref() {
2489 Some(client_rack) if self.partitioner.config().rack_aware => {
2490 let leader_rack = |partition: i32| self.leader_rack(topic, partition);
2491 let rack = RackPreference {
2492 leader_rack: &leader_rack,
2493 client_rack,
2494 };
2495 self.partitioner.update_rack_aware(
2496 topic,
2497 sticky,
2498 appended,
2499 partitions,
2500 enable_switch,
2501 rack,
2502 );
2503 }
2504 _ => self
2505 .partitioner
2506 .update(topic, sticky, appended, partitions, enable_switch),
2507 }
2508 }
2509
2510 fn partition_infos(&self, topic: &str, count: i32) -> (Vec<PartitionInfo>, Vec<PartitionInfo>) {
2514 let all: Vec<PartitionInfo> = (0..count)
2515 .map(|partition| {
2516 let leader_id = self
2517 .partition_leaders
2518 .get(&(topic.to_owned(), partition))
2519 .map(|leader| *leader)
2520 .filter(|leader| *leader >= 0);
2521 let leader_rack = leader_id.and_then(|id| self.leader_rack_of_broker(id));
2522 PartitionInfo {
2523 partition,
2524 leader_id,
2525 leader_rack,
2526 }
2527 })
2528 .collect();
2529 let available = all
2530 .iter()
2531 .filter(|info| info.leader_id.is_some())
2532 .cloned()
2533 .collect();
2534 (available, all)
2535 }
2536
2537 fn leader_rack_of_broker(&self, id: i32) -> Option<String> {
2540 let rack = self.broker_racks.get(&id)?;
2541 rack.clone()
2542 }
2543
2544 fn record_size_limit(&self, serialized_size: usize) -> Option<RecordSizeLimit> {
2547 if serialized_size > self.max_request_size {
2548 Some(RecordSizeLimit::MaxRequestSize(self.max_request_size))
2549 } else if serialized_size > self.buffer_pool.total() {
2550 Some(RecordSizeLimit::BufferMemory)
2551 } else {
2552 None
2553 }
2554 }
2555
2556 async fn lock_accumulator_for_append<'a>(
2561 &self,
2562 accumulator: &'a Mutex<Accumulator>,
2563 state: Option<TxnState>,
2564 expected_generation: Option<u64>,
2565 ) -> Result<MutexGuard<'a, Accumulator>, ProducerError> {
2566 let queue = accumulator.lock().await;
2567 if self.append_transaction_generation(state)? != expected_generation {
2568 return Err(ProducerError::RecoveryRequired);
2569 }
2570 Ok(queue)
2571 }
2572
2573 fn append_transaction_generation(
2586 &self,
2587 state: Option<TxnState>,
2588 ) -> Result<Option<u64>, ProducerError> {
2589 if state.is_some()
2590 && let Some(error) = self.transaction_error_state()
2591 {
2592 return Err(error);
2593 }
2594 self.transaction_generation(state)
2595 }
2596
2597 fn transaction_generation(
2608 &self,
2609 state: Option<TxnState>,
2610 ) -> Result<Option<u64>, ProducerError> {
2611 if self.transaction_recovery_required() {
2612 return Err(ProducerError::RecoveryRequired);
2613 }
2614 match state {
2615 Some(TxnState::InTransaction) => {
2616 Ok(Some(self.txn_recovery_generation.load(Ordering::Acquire)))
2617 }
2618 Some(TxnState::Preparing | TxnState::Prepared) => {
2619 Err(ProducerError::InvalidTransactionState(
2620 "send is not allowed after prepare_transaction",
2621 ))
2622 }
2623 Some(_) => Err(ProducerError::InvalidTransactionState(
2624 "cannot send records while no transaction is in progress",
2625 )),
2626 None => Ok(None),
2627 }
2628 }
2629
2630 #[tracing::instrument(
2642 level = "debug",
2643 skip_all,
2644 fields(topic = %topic, num_partitions = field::Empty),
2645 )]
2646 pub(crate) async fn partition_count(
2647 &self,
2648 topic: &str,
2649 partition: Option<i32>,
2650 ) -> Result<i32, ProducerError> {
2651 self.client.metadata_topics().add(topic);
2654 let count = self
2655 .metadata_wait()
2656 .partition_count(topic, partition, |topics| {
2657 self.refresh_producer_metadata(topics)
2658 })
2659 .await?;
2660 tracing::Span::current().record("num_partitions", count);
2661 Ok(count)
2662 }
2663
2664 fn metadata_wait(&self) -> MetadataWait<'_> {
2666 MetadataWait {
2667 cache: &self.metadata_cache,
2668 partition_leaders: &self.partition_leaders,
2669 refresh: &self.metadata_refresh,
2670 max_block: self.max_block,
2671 retry_backoff: self.init_retry_backoff.to_std(),
2672 max_backoff: self.retry_backoff_max.to_std(),
2673 }
2674 }
2675
2676 async fn refresh_producer_metadata(
2679 &self,
2680 topics: Vec<String>,
2681 ) -> Result<MetadataResponse, ClientError> {
2682 let response = self
2683 .client
2684 .refresh_metadata_with(metadata_request(topics))
2685 .await?;
2686 for broker in &response.brokers {
2689 self.broker_racks
2690 .insert(broker.node_id, broker.rack.clone());
2691 }
2692 Ok(response)
2693 }
2694
2695 #[tracing::instrument(
2696 level = "info",
2697 skip_all,
2698 fields(client_id = %self.client_id, transactional_id = self.transactional_id.as_deref()),
2699 err(level = "info"),
2700 )]
2701 pub async fn close(mut self) -> Result<(), ProducerError> {
2704 self.flush().await?;
2705 self.client.close_telemetry().await;
2707 self.state.store(STATE_CLOSED, Ordering::Release);
2708 self.sender_shutdown.cancel();
2709 if let Some(h) = self.sender_handle.take() {
2710 let _ = h.await;
2711 }
2712 Ok(())
2713 }
2714
2715 pub async fn client_instance_id(
2732 &self,
2733 timeout: Time,
2734 ) -> Result<Option<uuid::Uuid>, ProducerError> {
2735 Ok(self.client.client_instance_id(timeout).await?)
2736 }
2737
2738 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
2739 pub async fn flush(&self) -> Result<(), ProducerError> {
2742 self.is_active()?;
2743 let deadline = Instant::now()
2744 .checked_add(self.flush_timeout.duration())
2745 .ok_or(ProducerError::FlushTimeout)?;
2746
2747 time::timeout_at(deadline, async {
2748 let _ = self.wake_tx.send(DrainIntent::Force).await;
2749 loop {
2750 let notified = self.flush_notify.notified();
2751 tokio::pin!(notified);
2752 notified.as_mut().enable();
2753 if self.all_empty().await && self.in_flight.load(Ordering::Acquire) == 0 {
2754 return;
2755 }
2756 notified.await;
2757 }
2758 })
2759 .await
2760 .map_err(|_| ProducerError::FlushTimeout)
2761 }
2762
2763 async fn abort_undrained_batches(&self) -> Result<(), ProducerError> {
2774 self.is_active()?;
2775 for entry in self.accumulators.iter() {
2776 let mut a = entry.value().lock().await;
2777 a.abort_undrained_batches();
2778 }
2779
2780 let deadline = Instant::now()
2781 .checked_add(self.flush_timeout.duration())
2782 .ok_or(ProducerError::FlushTimeout)?;
2783 time::timeout_at(deadline, async {
2784 loop {
2785 let notified = self.flush_notify.notified();
2786 tokio::pin!(notified);
2787 notified.as_mut().enable();
2788 if self.in_flight.load(Ordering::Acquire) == 0 {
2789 return;
2790 }
2791 notified.await;
2792 }
2793 })
2794 .await
2795 .map_err(|_| ProducerError::FlushTimeout)
2796 }
2797
2798 async fn all_empty(&self) -> bool {
2799 for entry in self.accumulators.iter() {
2800 let a = entry.value().lock().await;
2801 if a.current.as_ref().is_some_and(|b| !b.is_empty()) {
2802 return false;
2803 }
2804 if !a.ready.is_empty() {
2805 return false;
2806 }
2807 }
2808 true
2809 }
2810}
2811
2812impl fmt::Debug for Producer {
2813 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2814 f.debug_struct("Producer")
2815 .field("producer_id", &self.producer_id())
2816 .field("producer_epoch", &self.producer_epoch())
2817 .field("transactional_id", &self.transactional_id)
2818 .field("compression", &self.compression)
2819 .field(
2820 "compression_level",
2821 &self.compression_levels.level(self.compression),
2822 )
2823 .finish_non_exhaustive()
2824 }
2825}
2826
2827fn current_millis() -> i64 {
2828 i64::try_from(
2829 SystemTime::now()
2830 .duration_since(UNIX_EPOCH)
2831 .map_or(0, |d| d.as_millis()),
2832 )
2833 .unwrap_or(0)
2834}
2835
2836fn build_topics_payload(
2845 offsets: &[((String, i32), i64)],
2846 topic_ids: &HashMap<String, Uuid>,
2847) -> Vec<TxnOffsetCommitRequestTopic> {
2848 let mut topics: Vec<TxnOffsetCommitRequestTopic> = Vec::new();
2849 for ((topic, partition), offset) in offsets {
2850 let partition = TxnOffsetCommitRequestPartition {
2851 partition_index: *partition,
2852 committed_offset: *offset,
2853 ..Default::default()
2854 };
2855 match topics.iter_mut().find(|entry| entry.name == *topic) {
2856 Some(entry) => entry.partitions.push(partition),
2857 None => topics.push(TxnOffsetCommitRequestTopic {
2858 name: topic.clone(),
2859 topic_id: topic_ids.get(topic).copied().unwrap_or(Uuid::ZERO),
2860 partitions: vec![partition],
2861 ..Default::default()
2862 }),
2863 }
2864 }
2865 topics
2866}
2867
2868#[cfg(test)]
2869mod tests {
2870 use std::{
2871 collections::BTreeSet,
2872 io,
2873 sync::{
2874 Arc, Mutex,
2875 atomic::{AtomicBool, AtomicU16, AtomicUsize, Ordering},
2876 },
2877 task::Poll,
2878 time::{Duration, Instant},
2879 };
2880
2881 use bytes::{Bytes, BytesMut};
2882 use krabka_client_core::MockBroker;
2883 use krabka_protocol::{
2884 Decode, Encode,
2885 owned::{
2886 api_versions_request,
2887 api_versions_response::{ApiVersion, ApiVersionsResponse},
2888 find_coordinator_request::{self, FindCoordinatorRequest},
2889 find_coordinator_response::{self, Coordinator, FindCoordinatorResponse},
2890 metadata_request::{self, MetadataRequest},
2891 metadata_response::{
2892 self, MetadataResponse, MetadataResponseBroker, MetadataResponsePartition,
2893 MetadataResponseTopic,
2894 },
2895 produce_request::{self, ProduceRequest},
2896 produce_response::{PartitionProduceResponse, ProduceResponse, TopicProduceResponse},
2897 },
2898 primitives::uuid::Uuid,
2899 };
2900 use tokio::{
2901 sync::{Mutex as AsyncMutex, mpsc},
2902 time,
2903 };
2904
2905 use super::{
2906 DrainIntent, Producer, TxnOffsetCommitCap, TxnOffsetCommitRequest,
2907 TxnOffsetCommitRequestPartition, TxnOffsetCommitRequestTopic, TxnOffsetCommitResponse,
2908 build_topics_payload, minted_identity, request_may_have_reached_the_broker,
2909 txn_offset_commit_error_code, wake_sender_after_append,
2910 };
2911 use crate::{
2912 ProducerRecord,
2913 accumulator::{Accumulator, AppendResult},
2914 builder::DEFAULT_PRODUCER_MAX_BLOCK,
2915 compression::Compression,
2916 error::ProducerError,
2917 partitioner::{PartitionInfo, Partitioner, partition_for_key},
2918 transactional::TxnState,
2919 };
2920
2921 struct FixedPartitioner {
2924 pick: i32,
2925 shown: Mutex<Vec<(Vec<PartitionInfo>, Vec<PartitionInfo>)>>,
2926 }
2927
2928 impl Partitioner for FixedPartitioner {
2929 fn partition(
2930 &self,
2931 _topic: &str,
2932 _key: Option<&[u8]>,
2933 _value: Option<&[u8]>,
2934 available_partitions: &[PartitionInfo],
2935 all_partitions: &[PartitionInfo],
2936 ) -> i32 {
2937 self.shown
2938 .lock()
2939 .unwrap()
2940 .push((available_partitions.to_vec(), all_partitions.to_vec()));
2941 self.pick
2942 }
2943 }
2944
2945 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2949 async fn a_custom_partitioner_picks_the_partition_and_a_negative_pick_fails_the_send() {
2950 let negative = "The partitioner generated an invalid partition number: -1. Partition \
2951 number should always be non-negative.";
2952 let leader = |partition| PartitionInfo {
2953 partition,
2954 leader_id: Some(1),
2955 leader_rack: None,
2956 };
2957 let shown_partitions = (0..4).map(leader).collect::<Vec<_>>();
2958 let cases = [
2959 ("keyless, pick 2", None, 2, Ok(2), vec![2]),
2960 (
2961 "keyed, pick 2",
2962 Some(b"kafka".as_slice()),
2963 2,
2964 Ok(2),
2965 vec![2],
2966 ),
2967 ("pick -1", None, -1, Err(negative.to_owned()), vec![]),
2968 ];
2969 let mut actual = Vec::new();
2970 let mut expected = Vec::new();
2971 for (name, key, pick, delivered, produced_partitions) in cases {
2972 let partitioner = Arc::new(FixedPartitioner {
2973 pick,
2974 shown: Mutex::new(Vec::new()),
2975 });
2976 let outcome = send_with_partitioner(
2977 vec![MetadataAnswer::Topic {
2978 error_code: 0,
2979 partitions: 4,
2980 }],
2981 Duration::from_secs(5),
2982 ProducerRecord {
2983 topic: METADATA_TOPIC.into(),
2984 key: key.map(Bytes::from_static),
2985 value: Some(Bytes::from_static(b"v")),
2986 ..Default::default()
2987 },
2988 Some(Arc::clone(&partitioner) as Arc<dyn Partitioner>),
2989 )
2990 .await;
2991 actual.push((
2992 name,
2993 outcome.delivered,
2994 outcome.produced_partitions,
2995 partitioner.shown.lock().unwrap().clone(),
2996 ));
2997 expected.push((
2998 name,
2999 delivered,
3000 produced_partitions,
3001 vec![(shown_partitions.clone(), shown_partitions.clone())],
3002 ));
3003 }
3004 assert2::assert!(actual == expected);
3005 }
3006
3007 #[test]
3011 fn a_minted_identity_needs_both_halves_of_the_pair() {
3012 assert2::check!(minted_identity((4242, 7)) == Some((4242, 7)));
3013 assert2::check!(minted_identity((0, 0)) == Some((0, 0)));
3014 assert2::check!(minted_identity((-1, -1)).is_none());
3015 assert2::check!(minted_identity((4242, -1)).is_none());
3017 assert2::check!(minted_identity((-1, 7)).is_none());
3018 }
3019
3020 const CLIENT_ID: &str = "producer-test";
3021
3022 #[test]
3023 fn only_new_deadlines_and_rollovers_wake_nonzero_linger() {
3024 let (wake_tx, mut wake_rx) = mpsc::channel(4);
3025
3026 let mut coalesced = Accumulator::new(1024);
3027 let AppendResult { wakes_sender, .. } =
3028 coalesced.try_append(None, Some(Bytes::from_static(b"a")), vec![], 0, None);
3029 wake_sender_after_append(&wake_tx, krabka_units::millis(10), wakes_sender);
3030 assert_eq!(wake_rx.try_recv(), Ok(DrainIntent::Ready));
3031 let AppendResult { wakes_sender, .. } =
3032 coalesced.try_append(None, Some(Bytes::from_static(b"b")), vec![], 0, None);
3033 drop(coalesced);
3034 wake_sender_after_append(&wake_tx, krabka_units::millis(10), wakes_sender);
3035 assert_eq!(wake_rx.try_recv(), Err(mpsc::error::TryRecvError::Empty));
3036
3037 let mut rollover = Accumulator::new(20);
3038 let _ = rollover.try_append(None, Some(Bytes::from_static(b"a")), vec![], 0, None);
3039 let AppendResult { wakes_sender, .. } =
3040 rollover.try_append(None, Some(Bytes::from_static(b"b")), vec![], 0, None);
3041 drop(rollover);
3042 wake_sender_after_append(&wake_tx, krabka_units::millis(10), wakes_sender);
3043 assert_eq!(wake_rx.try_recv(), Ok(DrainIntent::Ready));
3044
3045 let mut immediate = Accumulator::new(1024);
3046 let _ = immediate.try_append(None, Some(Bytes::from_static(b"a")), vec![], 0, None);
3047 drop(immediate);
3048 wake_sender_after_append(&wake_tx, krabka_units::secs(0), false);
3049 assert_eq!(wake_rx.try_recv(), Ok(DrainIntent::Force));
3050 }
3051
3052 fn encode_v0(resp: &impl Encode) -> Vec<u8> {
3053 let mut buf = BytesMut::new();
3054 resp.encode(&mut buf, 0).unwrap();
3055 buf.to_vec()
3056 }
3057
3058 fn encode_find_coordinator_response(version: i16) -> Vec<u8> {
3059 let resp = if version >= 4 {
3060 FindCoordinatorResponse {
3061 coordinators: vec![Coordinator {
3062 key: "group-a".into(),
3063 node_id: 1,
3064 host: "127.0.0.1".into(),
3065 port: 19092,
3066 ..Default::default()
3067 }],
3068 ..Default::default()
3069 }
3070 } else {
3071 FindCoordinatorResponse {
3072 node_id: 1,
3073 host: "127.0.0.1".into(),
3074 port: 19092,
3075 ..Default::default()
3076 }
3077 };
3078 let mut buf = BytesMut::new();
3079 if version >= find_coordinator_response::FLEXIBLE_MIN {
3080 buf.extend_from_slice(&[0]);
3081 }
3082 resp.encode(&mut buf, version).unwrap();
3083 buf.to_vec()
3084 }
3085
3086 fn api_versions_response(find_coordinator_version: i16) -> Vec<u8> {
3087 encode_v0(&ApiVersionsResponse {
3088 api_keys: vec![
3089 ApiVersion {
3090 api_key: api_versions_request::API_KEY,
3091 min_version: 0,
3092 max_version: 0,
3093 ..Default::default()
3094 },
3095 ApiVersion {
3096 api_key: find_coordinator_request::API_KEY,
3097 min_version: find_coordinator_version,
3098 max_version: find_coordinator_version,
3099 ..Default::default()
3100 },
3101 ],
3102 ..Default::default()
3103 })
3104 }
3105
3106 fn decode_request_body(body: &[u8], version: i16) -> FindCoordinatorRequest {
3107 let header_len = 2 + CLIENT_ID.len() + usize::from(version >= 3);
3108 let mut request_body = &body[header_len..];
3109 FindCoordinatorRequest::decode(&mut request_body, version).unwrap()
3110 }
3111
3112 async fn producer_with_find_coordinator_version(
3113 version: i16,
3114 seen: Arc<Mutex<Vec<FindCoordinatorRequest>>>,
3115 ) -> (MockBroker, Producer) {
3116 let seen_by_handler = Arc::clone(&seen);
3117 let find_coordinator_version = version;
3118 let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3119 if api_key == api_versions_request::API_KEY {
3120 return Some(api_versions_response(find_coordinator_version));
3121 }
3122 if api_key == find_coordinator_request::API_KEY {
3123 seen_by_handler
3124 .lock()
3125 .unwrap()
3126 .push(decode_request_body(body, version));
3127 return Some(encode_find_coordinator_response(version));
3128 }
3129 None
3130 })
3131 .await
3132 .unwrap();
3133 let producer = Producer::builder()
3134 .bootstrap(mock.addr.to_string())
3135 .client_id(CLIENT_ID)
3136 .enable_idempotence(false)
3137 .build()
3138 .await
3139 .expect("producer connects to mock broker");
3140 (mock, producer)
3141 }
3142
3143 async fn producer_with_flush_timeout(flush_timeout: Duration) -> (MockBroker, Producer) {
3144 let mock = MockBroker::start(|api_key, _version, _corr_id, _body| {
3145 (api_key == api_versions_request::API_KEY).then(|| api_versions_response(4))
3146 })
3147 .await
3148 .unwrap();
3149 let producer = Producer::builder()
3150 .bootstrap(mock.addr.to_string())
3151 .client_id(CLIENT_ID)
3152 .enable_idempotence(false)
3153 .flush_timeout(flush_timeout)
3154 .build()
3155 .await
3156 .expect("producer connects to mock broker");
3157 (mock, producer)
3158 }
3159
3160 #[derive(Clone, Copy)]
3161 enum LookupKind {
3162 Group,
3163 Transaction,
3164 }
3165
3166 async fn find_coordinator(producer: &Producer, kind: LookupKind, key: &str) -> String {
3167 let deadline = time::Instant::now() + Duration::from_secs(5);
3168 match kind {
3169 LookupKind::Group => producer.find_group_coordinator(key, deadline).await,
3170 LookupKind::Transaction => producer.find_txn_coordinator(key, deadline).await,
3171 }
3172 .expect("coordinator is returned")
3173 }
3174
3175 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3176 async fn find_coordinator_sends_expected_request_for_legacy_and_batched_versions() {
3177 for (_name, version, kind, key, expected_request) in [
3178 (
3179 "legacy group",
3180 3,
3181 LookupKind::Group,
3182 "group-a",
3183 FindCoordinatorRequest {
3184 key: "group-a".into(),
3185 ..Default::default()
3186 },
3187 ),
3188 (
3189 "batched group",
3190 4,
3191 LookupKind::Group,
3192 "group-a",
3193 FindCoordinatorRequest {
3194 coordinator_keys: vec!["group-a".into()],
3195 ..Default::default()
3196 },
3197 ),
3198 (
3199 "legacy transaction",
3200 3,
3201 LookupKind::Transaction,
3202 "txn-a",
3203 FindCoordinatorRequest {
3204 key: "txn-a".into(),
3205 key_type: 1,
3206 ..Default::default()
3207 },
3208 ),
3209 (
3210 "batched transaction",
3211 4,
3212 LookupKind::Transaction,
3213 "txn-a",
3214 FindCoordinatorRequest {
3215 key_type: 1,
3216 coordinator_keys: vec!["txn-a".into()],
3217 ..Default::default()
3218 },
3219 ),
3220 ] {
3221 let seen = Arc::new(Mutex::new(Vec::new()));
3222 let (mock, producer) =
3223 producer_with_find_coordinator_version(version, Arc::clone(&seen)).await;
3224
3225 let addr = find_coordinator(&producer, kind, key).await;
3226 assert2::assert!(addr == "127.0.0.1:19092");
3227 assert2::assert!(*seen.lock().unwrap() == vec![expected_request]);
3228 mock.stop();
3229 }
3230 }
3231
3232 #[tokio::test(start_paused = true)]
3233 async fn flush_times_out_at_the_configured_deadline() {
3234 let (mock, producer) = producer_with_flush_timeout(Duration::from_millis(7)).await;
3235 producer.in_flight.store(1, Ordering::Release);
3236
3237 let flush = producer.flush();
3238 tokio::pin!(flush);
3239 assert!(futures::poll!(flush.as_mut()).is_pending());
3240
3241 time::advance(Duration::from_millis(6)).await;
3242 assert!(futures::poll!(flush.as_mut()).is_pending());
3243
3244 time::advance(Duration::from_millis(1)).await;
3245 assert!(matches!(
3246 futures::poll!(flush.as_mut()),
3247 Poll::Ready(Err(ProducerError::FlushTimeout))
3248 ));
3249 mock.stop();
3250 }
3251
3252 #[tokio::test(start_paused = true)]
3253 async fn flush_timeout_bounds_a_blocked_force_wake() {
3254 let (mock, mut producer) = producer_with_flush_timeout(Duration::from_millis(7)).await;
3255 let (wake_tx, _wake_rx) = mpsc::channel(16);
3256 producer.wake_tx = wake_tx;
3257 for _ in 0..16 {
3258 producer
3259 .wake_tx
3260 .try_send(DrainIntent::Force)
3261 .expect("wake channel has capacity");
3262 }
3263
3264 let flush = producer.flush();
3265 tokio::pin!(flush);
3266 assert!(futures::poll!(flush.as_mut()).is_pending());
3267
3268 time::advance(Duration::from_millis(6)).await;
3269 assert!(futures::poll!(flush.as_mut()).is_pending());
3270
3271 time::advance(Duration::from_millis(1)).await;
3272 assert!(matches!(
3273 futures::poll!(flush.as_mut()),
3274 Poll::Ready(Err(ProducerError::FlushTimeout))
3275 ));
3276 mock.stop();
3277 }
3278
3279 #[tokio::test]
3280 async fn flush_does_not_miss_notification_during_state_check() {
3281 let (mock, producer) = producer_with_flush_timeout(Duration::from_millis(20)).await;
3282 let accumulator = Arc::new(AsyncMutex::new(Accumulator::new(1024)));
3283 producer
3284 .accumulators
3285 .insert(("held".to_owned(), 0), Arc::clone(&accumulator));
3286 let mut guard = accumulator.lock().await;
3287
3288 let flush = producer.flush();
3289 tokio::pin!(flush);
3290 assert!(futures::poll!(flush.as_mut()).is_pending());
3291
3292 guard.current = None;
3293 guard.ready.clear();
3294 producer.flush_notify.notify_waiters();
3295 drop(guard);
3296 drop(accumulator);
3297
3298 time::timeout(Duration::from_millis(20), flush)
3299 .await
3300 .expect("flush must not wait for another notification")
3301 .expect("empty producer flushes");
3302 mock.stop();
3303 }
3304
3305 #[tokio::test]
3306 async fn append_waiting_for_a_swept_queue_rejects_its_old_generation() {
3307 let (mock, producer) = producer_with_flush_timeout(Duration::from_millis(20)).await;
3308 let accumulator = AsyncMutex::new(Accumulator::new(1024));
3309 let transaction = Some(TxnState::InTransaction);
3310 {
3311 let queue = accumulator.lock().await;
3312 let expected = producer.append_transaction_generation(transaction).unwrap();
3313 let append = producer.lock_accumulator_for_append(&accumulator, transaction, expected);
3314 tokio::pin!(append);
3315 assert2::assert!(futures::poll!(append.as_mut()).is_pending());
3316
3317 producer.require_transaction_recovery();
3322 producer
3323 .txn_recovery_required
3324 .store(false, Ordering::Release);
3325 drop(queue);
3326 assert2::assert!(matches!(append.await, Err(ProducerError::RecoveryRequired)));
3327 }
3328 assert2::assert!(accumulator.lock().await.current.is_none());
3329 let current = producer.append_transaction_generation(transaction).unwrap();
3330 let fresh = producer
3331 .lock_accumulator_for_append(&accumulator, transaction, current)
3332 .await
3333 .expect("a new-generation append can acquire the swept queue");
3334 assert2::assert!(fresh.current.is_none());
3335 drop(fresh);
3336 drop(accumulator);
3337 mock.stop();
3338 }
3339
3340 #[derive(Debug, PartialEq, Eq)]
3342 struct TxnOffsetCommitShape {
3343 cap: TxnOffsetCommitCap,
3344 max_version: i16,
3345 wire_topics: Vec<TxnOffsetCommitRequestTopic>,
3347 }
3348
3349 #[test]
3356 fn txn_offset_commit_names_topics_by_id_only_at_v6() {
3357 let orders = Uuid([7; 16]);
3358 let payments = Uuid([8; 16]);
3359 let offsets = [
3360 (("orders".to_owned(), 0), 10),
3361 (("payments".to_owned(), 1), 20),
3362 (("orders".to_owned(), 2), 30),
3363 ];
3364 let partition = |partition_index, committed_offset| TxnOffsetCommitRequestPartition {
3365 partition_index,
3366 committed_offset,
3367 ..Default::default()
3368 };
3369 let topic = |name: &str, topic_id, partitions| TxnOffsetCommitRequestTopic {
3370 name: name.to_owned(),
3371 topic_id,
3372 partitions,
3373 ..Default::default()
3374 };
3375 let both_ids =
3376 maplit::hashmap! {"orders".to_owned() => orders, "payments".to_owned() => payments};
3377 let one_id = maplit::hashmap! {"orders".to_owned() => orders};
3378 for (name, topic_ids, transaction_v2, expected) in [
3379 (
3380 "every topic has an id",
3381 &both_ids,
3382 true,
3383 TxnOffsetCommitShape {
3384 cap: TxnOffsetCommitCap::Latest,
3385 max_version: 6,
3386 wire_topics: vec![
3387 topic("", orders, vec![partition(0, 10), partition(2, 30)]),
3388 topic("", payments, vec![partition(1, 20)]),
3389 ],
3390 },
3391 ),
3392 (
3393 "one topic has no id",
3394 &one_id,
3395 true,
3396 TxnOffsetCommitShape {
3397 cap: TxnOffsetCommitCap::TopicNames,
3398 max_version: 5,
3399 wire_topics: vec![
3400 topic(
3401 "orders",
3402 Uuid::ZERO,
3403 vec![partition(0, 10), partition(2, 30)],
3404 ),
3405 topic("payments", Uuid::ZERO, vec![partition(1, 20)]),
3406 ],
3407 },
3408 ),
3409 (
3410 "transaction version 1",
3411 &both_ids,
3412 false,
3413 TxnOffsetCommitShape {
3414 cap: TxnOffsetCommitCap::TransactionV1,
3415 max_version: 4,
3416 wire_topics: vec![
3417 topic(
3418 "orders",
3419 Uuid::ZERO,
3420 vec![partition(0, 10), partition(2, 30)],
3421 ),
3422 topic("payments", Uuid::ZERO, vec![partition(1, 20)]),
3423 ],
3424 },
3425 ),
3426 ] {
3427 let request = TxnOffsetCommitRequest {
3428 transactional_id: "txn".into(),
3429 group_id: "group".into(),
3430 topics: build_topics_payload(&offsets, topic_ids),
3431 ..Default::default()
3432 };
3433 let cap = TxnOffsetCommitCap::of(&request.topics, transaction_v2);
3434 let max_version = cap.max_version();
3435 let mut buf = BytesMut::new();
3436 request.encode(&mut buf, max_version).expect("encode");
3437 let decoded =
3438 TxnOffsetCommitRequest::decode(&mut buf.freeze(), max_version).expect("decode");
3439 let actual = TxnOffsetCommitShape {
3440 cap,
3441 max_version,
3442 wire_topics: decoded.topics,
3443 };
3444 assert2::assert!(actual == expected, "{name}");
3445 }
3446 }
3447
3448 #[test]
3453 fn txn_offset_commit_reads_the_code_that_decides_the_answer() {
3454 use krabka_protocol::owned::txn_offset_commit_response::{
3455 TxnOffsetCommitResponsePartition, TxnOffsetCommitResponseTopic,
3456 };
3457 let response = |codes: &[i16]| TxnOffsetCommitResponse {
3458 topics: vec![TxnOffsetCommitResponseTopic {
3459 name: "topic".into(),
3460 partitions: codes
3461 .iter()
3462 .enumerate()
3463 .map(|(index, error_code)| TxnOffsetCommitResponsePartition {
3464 partition_index: i32::try_from(index).expect("partition index"),
3465 error_code: *error_code,
3466 ..Default::default()
3467 })
3468 .collect(),
3469 ..Default::default()
3470 }],
3471 ..Default::default()
3472 };
3473 for (name, codes, expected) in [
3474 ("every row committed", &[0, 0][..], 0),
3475 ("one retriable row", &[0, 14][..], 14),
3476 ("the coordinator moved", &[14, 16][..], 16),
3477 ("a timed out row asks for the coordinator", &[3, 7][..], 7),
3478 ("a code that no rule retries wins", &[16, 48][..], 48),
3479 ("an abortable code wins", &[14, 22][..], 22),
3480 ("the first row that resends stands", &[3, 14][..], 3),
3481 ] {
3482 assert2::assert!(
3483 txn_offset_commit_error_code(&response(codes)) == expected,
3484 "{name}"
3485 );
3486 }
3487 }
3488
3489 #[derive(Clone, Copy)]
3491 enum MetadataAnswer {
3492 Topic { error_code: i16, partitions: i32 },
3494 Silent,
3496 }
3497
3498 #[derive(Debug, PartialEq, Eq)]
3502 struct SendOutcome {
3503 delivered: Result<i32, String>,
3504 produced_partitions: Vec<i32>,
3505 metadata_requests: BTreeSet<RequestedTopics>,
3506 }
3507
3508 type RequestedTopics = (Option<Vec<String>>, bool);
3511
3512 fn requested_topics(body: &[u8], version: i16) -> MetadataRequest {
3513 let header_len =
3514 2 + CLIENT_ID.len() + usize::from(version >= metadata_request::FLEXIBLE_MIN);
3515 let mut request_body = &body[header_len..];
3516 MetadataRequest::decode(&mut request_body, version).expect("decode Metadata")
3517 }
3518
3519 const METADATA_TOPIC: &str = "orders";
3520
3521 fn metadata_answer(
3525 version: i16,
3526 port: u16,
3527 answer: MetadataAnswer,
3528 named: bool,
3529 ) -> Option<Vec<u8>> {
3530 let MetadataAnswer::Topic {
3531 error_code,
3532 partitions,
3533 } = answer
3534 else {
3535 return None;
3536 };
3537 let listed = named || error_code == 0;
3538 let response = MetadataResponse {
3539 brokers: vec![MetadataResponseBroker {
3540 node_id: 1,
3541 host: "127.0.0.1".into(),
3542 port: i32::from(port),
3543 ..Default::default()
3544 }],
3545 topics: listed
3546 .then(|| MetadataResponseTopic {
3547 error_code,
3548 name: Some(METADATA_TOPIC.into()),
3549 partitions: (0..partitions)
3550 .map(|partition_index| MetadataResponsePartition {
3551 partition_index,
3552 leader_id: 1,
3553 ..Default::default()
3554 })
3555 .collect(),
3556 ..Default::default()
3557 })
3558 .into_iter()
3559 .collect(),
3560 ..Default::default()
3561 };
3562 let mut buf = BytesMut::new();
3563 if version >= metadata_response::FLEXIBLE_MIN {
3564 buf.extend_from_slice(&[0]);
3565 }
3566 response.encode(&mut buf, version).expect("encode Metadata");
3567 Some(buf.to_vec())
3568 }
3569
3570 fn produce_answer(request: &ProduceRequest) -> Vec<u8> {
3571 let response = ProduceResponse {
3572 responses: request
3573 .topic_data
3574 .iter()
3575 .map(|topic| TopicProduceResponse {
3576 name: topic.name.clone(),
3577 partition_responses: topic
3578 .partition_data
3579 .iter()
3580 .map(|partition| PartitionProduceResponse {
3581 index: partition.index,
3582 base_offset: 0,
3583 log_start_offset: -1,
3584 ..Default::default()
3585 })
3586 .collect(),
3587 ..Default::default()
3588 })
3589 .collect(),
3590 ..Default::default()
3591 };
3592 let mut buf = BytesMut::new();
3593 response
3594 .encode(&mut buf, PRODUCE_VERSION)
3595 .expect("encode Produce");
3596 buf.to_vec()
3597 }
3598
3599 const PRODUCE_VERSION: i16 = 3;
3600
3601 async fn send_against_scripted_metadata(
3604 answers: Vec<MetadataAnswer>,
3605 max_block: Duration,
3606 record: ProducerRecord,
3607 ) -> SendOutcome {
3608 send_with_partitioner(answers, max_block, record, None).await
3609 }
3610
3611 async fn send_with_partitioner(
3613 answers: Vec<MetadataAnswer>,
3614 max_block: Duration,
3615 record: ProducerRecord,
3616 partitioner: Option<Arc<dyn Partitioner>>,
3617 ) -> SendOutcome {
3618 let port = Arc::new(AtomicU16::new(0));
3619 let handler_port = Arc::clone(&port);
3620 let metadata_requests = AtomicUsize::new(0);
3621 let metadata_log = Arc::new(Mutex::new(BTreeSet::new()));
3622 let handler_metadata_log = Arc::clone(&metadata_log);
3623 let produce_log = Arc::new(Mutex::new(Vec::new()));
3624 let handler_produce_log = Arc::clone(&produce_log);
3625 let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3626 if api_key == api_versions_request::API_KEY {
3627 return Some(encode_v0(&ApiVersionsResponse {
3628 api_keys: vec![
3629 ApiVersion {
3630 api_key: metadata_request::API_KEY,
3631 min_version: 0,
3632 max_version: 12,
3633 ..Default::default()
3634 },
3635 ApiVersion {
3636 api_key: produce_request::API_KEY,
3637 min_version: PRODUCE_VERSION,
3638 max_version: PRODUCE_VERSION,
3639 ..Default::default()
3640 },
3641 ],
3642 ..Default::default()
3643 }));
3644 }
3645 if api_key == metadata_request::API_KEY {
3646 let index = metadata_requests.fetch_add(1, Ordering::SeqCst);
3647 let answer = answers[index.min(answers.len() - 1)];
3648 let request = requested_topics(body, version);
3649 let topics = request.topics.map(|topics| {
3650 topics
3651 .into_iter()
3652 .map(|topic| topic.name.unwrap_or_default())
3653 .collect::<Vec<_>>()
3654 });
3655 let named = topics
3656 .as_ref()
3657 .is_some_and(|topics| topics.iter().any(|topic| topic == METADATA_TOPIC));
3658 handler_metadata_log
3659 .lock()
3660 .unwrap()
3661 .insert((topics, request.allow_auto_topic_creation));
3662 return metadata_answer(
3663 version,
3664 handler_port.load(Ordering::SeqCst),
3665 answer,
3666 named,
3667 );
3668 }
3669 if api_key == produce_request::API_KEY {
3670 let mut request_body = &body[2 + CLIENT_ID.len()..];
3671 let request =
3672 ProduceRequest::decode(&mut request_body, version).expect("decode Produce");
3673 handler_produce_log.lock().unwrap().extend(
3674 request
3675 .topic_data
3676 .iter()
3677 .flat_map(|topic| topic.partition_data.iter().map(|p| p.index)),
3678 );
3679 return Some(produce_answer(&request));
3680 }
3681 None
3682 })
3683 .await
3684 .unwrap();
3685 port.store(mock.addr.port(), Ordering::SeqCst);
3686 let producer = Producer::builder()
3687 .bootstrap(mock.addr.to_string())
3688 .client_id(CLIENT_ID)
3689 .enable_idempotence(false)
3690 .request_timeout(Duration::from_millis(300))
3691 .metadata_recovery_strategy(krabka_client_core::MetadataRecoveryStrategy::None)
3694 .max_block(max_block)
3695 .maybe_partitioner(partitioner)
3696 .build()
3697 .await
3698 .expect("producer connects to mock broker");
3699 let delivered = producer
3700 .send(record)
3701 .await
3702 .map(|metadata| metadata.partition)
3703 .map_err(|error| error.to_string());
3704 producer.flush().await.expect("flush");
3705 let outcome = SendOutcome {
3706 delivered,
3707 produced_partitions: produce_log.lock().unwrap().clone(),
3708 metadata_requests: metadata_log.lock().unwrap().clone(),
3709 };
3710 producer
3711 .close()
3712 .await
3713 .map(|()| {
3714 mock.stop();
3715 outcome
3716 })
3717 .expect("close producer")
3718 }
3719
3720 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3728 async fn send_waits_for_topic_metadata_up_to_max_block() {
3729 const UNKNOWN_TOPIC_OR_PARTITION: i16 = 3;
3730 const TOPIC_AUTHORIZATION_FAILED: i16 = 29;
3731 let keyed = |key: &'static [u8]| ProducerRecord {
3732 topic: METADATA_TOPIC.into(),
3733 key: Some(Bytes::from_static(key)),
3734 value: Some(Bytes::from_static(b"v")),
3735 ..Default::default()
3736 };
3737 let pinned = |partition: i32| ProducerRecord {
3738 topic: METADATA_TOPIC.into(),
3739 partition: Some(partition),
3740 value: Some(Bytes::from_static(b"v")),
3741 ..Default::default()
3742 };
3743 let topic = |error_code: i16, partitions: i32| MetadataAnswer::Topic {
3744 error_code,
3745 partitions,
3746 };
3747 let orders_request = || maplit::btreeset! {(Some(vec![METADATA_TOPIC.to_owned()]), true)};
3750 let delivered = |partition: i32| SendOutcome {
3751 delivered: Ok(partition),
3752 produced_partitions: vec![partition],
3753 metadata_requests: orders_request(),
3754 };
3755 let failed = |error: &str| SendOutcome {
3756 delivered: Err(error.to_owned()),
3757 produced_partitions: vec![],
3758 metadata_requests: orders_request(),
3759 };
3760 let default_block = DEFAULT_PRODUCER_MAX_BLOCK;
3761 let short_block = Duration::from_millis(100);
3762 let cases = [
3763 (
3764 "topic appears on the second refresh",
3765 vec![topic(UNKNOWN_TOPIC_OR_PARTITION, 0), topic(0, 12)],
3766 default_block,
3767 keyed(b"kafka"),
3768 delivered(partition_for_key(b"kafka", 12)),
3769 ),
3770 (
3771 "topic never appears",
3772 vec![topic(UNKNOWN_TOPIC_OR_PARTITION, 0)],
3773 short_block,
3774 keyed(b"kafka"),
3775 failed("Topic orders not present in metadata after 100 ms."),
3776 ),
3777 (
3778 "metadata transport error, then the topic",
3779 vec![MetadataAnswer::Silent, topic(0, 12)],
3780 default_block,
3781 keyed(b"my-key"),
3782 delivered(9),
3783 ),
3784 (
3785 "explicit partition beyond the count",
3786 vec![topic(0, 4)],
3787 short_block,
3788 pinned(7),
3789 failed(
3790 "Partition 7 of topic orders with partition count 4 is not present in metadata after 100 ms.",
3791 ),
3792 ),
3793 (
3794 "explicit partition after the partition count grows",
3795 vec![topic(0, 4), topic(0, 12)],
3796 default_block,
3797 pinned(7),
3798 delivered(7),
3799 ),
3800 (
3801 "topic authorization failed fails at once",
3802 vec![topic(TOPIC_AUTHORIZATION_FAILED, 0)],
3803 default_block,
3804 keyed(b"kafka"),
3805 failed("broker error_code 29"),
3806 ),
3807 ];
3808 let mut actual = Vec::new();
3809 let mut wanted = Vec::new();
3810 for (name, answers, max_block, record, expected) in cases {
3811 let outcome = send_against_scripted_metadata(answers, max_block, record).await;
3812 actual.push((name, outcome));
3813 wanted.push((name, expected));
3814 }
3815 assert2::assert!(actual == wanted);
3816 }
3817
3818 #[derive(Debug, PartialEq, Eq)]
3820 struct BlockedSendOutcome {
3821 blocked: bool,
3824 delivered: Result<(), String>,
3826 }
3827
3828 async fn three_partition_broker(
3832 answer_produce: Arc<AtomicBool>,
3833 ) -> (MockBroker, Arc<AtomicUsize>) {
3834 let port = Arc::new(AtomicU16::new(0));
3835 let handler_port = Arc::clone(&port);
3836 let handler_answer = answer_produce;
3837 let produce_requests = Arc::new(AtomicUsize::new(0));
3838 let handler_produce_requests = Arc::clone(&produce_requests);
3839 let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3840 if api_key == api_versions_request::API_KEY {
3841 return Some(encode_v0(&ApiVersionsResponse {
3842 api_keys: vec![
3843 ApiVersion {
3844 api_key: metadata_request::API_KEY,
3845 min_version: 0,
3846 max_version: 12,
3847 ..Default::default()
3848 },
3849 ApiVersion {
3850 api_key: produce_request::API_KEY,
3851 min_version: PRODUCE_VERSION,
3852 max_version: PRODUCE_VERSION,
3853 ..Default::default()
3854 },
3855 ],
3856 ..Default::default()
3857 }));
3858 }
3859 if api_key == metadata_request::API_KEY {
3860 return metadata_answer(
3861 version,
3862 handler_port.load(Ordering::SeqCst),
3863 MetadataAnswer::Topic {
3864 error_code: 0,
3865 partitions: 3,
3866 },
3867 true,
3868 );
3869 }
3870 if api_key == produce_request::API_KEY {
3871 handler_produce_requests.fetch_add(1, Ordering::SeqCst);
3872 }
3873 if api_key == produce_request::API_KEY && handler_answer.load(Ordering::SeqCst) {
3874 let mut request_body = &body[2 + CLIENT_ID.len()..];
3875 let request =
3876 ProduceRequest::decode(&mut request_body, version).expect("decode Produce");
3877 return Some(produce_answer(&request));
3878 }
3879 None
3880 })
3881 .await
3882 .unwrap();
3883 port.store(mock.addr.port(), Ordering::SeqCst);
3884 (mock, produce_requests)
3885 }
3886
3887 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3891 async fn client_metadata_refreshes_name_the_topics_of_the_sends() {
3892 let named = |topics: &[&str]| {
3893 krabka_client_core::topics_request(topics.iter().map(|&t| t.to_owned()), true)
3894 };
3895 for (name, sends, expected) in [
3896 ("no send", vec![], named(&[])),
3897 ("send to t1", vec!["t1"], named(&["t1"])),
3898 (
3899 "send to t1, then t2",
3900 vec!["t1", "t2", "t1"],
3901 named(&["t1", "t2"]),
3902 ),
3903 ] {
3904 let port = Arc::new(AtomicU16::new(0));
3905 let handler_port = Arc::clone(&port);
3906 let requests = Arc::new(Mutex::new(Vec::new()));
3907 let handler_requests = Arc::clone(&requests);
3908 let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
3909 if api_key == api_versions_request::API_KEY {
3910 return Some(encode_v0(&ApiVersionsResponse {
3911 api_keys: vec![ApiVersion {
3912 api_key: metadata_request::API_KEY,
3913 min_version: 0,
3914 max_version: 12,
3915 ..Default::default()
3916 }],
3917 ..Default::default()
3918 }));
3919 }
3920 if api_key != metadata_request::API_KEY {
3921 return None;
3922 }
3923 let request = requested_topics(body, version);
3924 let topics = request.topics.clone().unwrap_or_default();
3925 handler_requests.lock().unwrap().push(request);
3926 let response = MetadataResponse {
3927 brokers: vec![MetadataResponseBroker {
3928 node_id: 1,
3929 host: "127.0.0.1".into(),
3930 port: i32::from(handler_port.load(Ordering::SeqCst)),
3931 ..Default::default()
3932 }],
3933 topics: topics
3934 .into_iter()
3935 .map(|topic| MetadataResponseTopic {
3936 name: topic.name,
3937 partitions: vec![MetadataResponsePartition {
3938 leader_id: 1,
3939 ..Default::default()
3940 }],
3941 ..Default::default()
3942 })
3943 .collect(),
3944 ..Default::default()
3945 };
3946 let mut buf = BytesMut::new();
3947 if version >= metadata_response::FLEXIBLE_MIN {
3948 buf.extend_from_slice(&[0]);
3949 }
3950 response.encode(&mut buf, version).expect("encode Metadata");
3951 Some(buf.to_vec())
3952 })
3953 .await
3954 .unwrap();
3955 port.store(mock.addr.port(), Ordering::SeqCst);
3956 let producer = Producer::builder()
3957 .bootstrap(mock.addr.to_string())
3958 .client_id(CLIENT_ID)
3959 .enable_idempotence(false)
3960 .max_block(Duration::from_secs(2))
3961 .build()
3962 .await
3963 .expect("producer connects to mock broker");
3964 for topic in sends {
3965 producer
3966 .partition_count(topic, None)
3967 .await
3968 .expect("topic metadata");
3969 }
3970 producer
3971 .client
3972 .refresh_metadata()
3973 .await
3974 .expect("refresh metadata");
3975 let last = requests.lock().unwrap().last().cloned();
3976 mock.stop();
3977 drop(producer);
3978 assert2::check!(last == Some(expected), "{name}");
3979 }
3980 }
3981
3982 #[derive(Debug, PartialEq, Eq)]
3985 struct ValidatedSend {
3986 delivered: Result<(), String>,
3987 produce_requests: usize,
3988 }
3989
3990 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3994 async fn send_rejects_a_negative_partition_or_timestamp() {
3995 let cases = [
3996 (
3997 "negative partition",
3998 Some(-1),
3999 None,
4000 ValidatedSend {
4001 delivered: Err("Invalid partition: -1. Partition number should always be \
4002 non-negative or null."
4003 .to_owned()),
4004 produce_requests: 0,
4005 },
4006 ),
4007 (
4008 "negative timestamp",
4009 None,
4010 Some(-5),
4011 ValidatedSend {
4012 delivered: Err("Invalid timestamp: -5. Timestamp should always be \
4013 non-negative or null."
4014 .to_owned()),
4015 produce_requests: 0,
4016 },
4017 ),
4018 (
4019 "both negative reports the timestamp first",
4020 Some(-2),
4021 Some(-3),
4022 ValidatedSend {
4023 delivered: Err("Invalid timestamp: -3. Timestamp should always be \
4024 non-negative or null."
4025 .to_owned()),
4026 produce_requests: 0,
4027 },
4028 ),
4029 (
4030 "partition 0 and timestamp 0",
4031 Some(0),
4032 Some(0),
4033 ValidatedSend {
4034 delivered: Ok(()),
4035 produce_requests: 1,
4036 },
4037 ),
4038 ];
4039 for (name, partition, timestamp_ms, expected) in cases {
4040 let (mock, produce_requests) =
4041 three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4042 let producer = Producer::builder()
4043 .bootstrap(mock.addr.to_string())
4044 .client_id(CLIENT_ID)
4045 .enable_idempotence(false)
4046 .max_block(Duration::from_secs(2))
4047 .build()
4048 .await
4049 .expect("producer connects to mock broker");
4050 let receiver = producer.send(ProducerRecord {
4051 topic: METADATA_TOPIC.into(),
4052 partition,
4053 timestamp_ms,
4054 value: Some(Bytes::from_static(b"v")),
4055 ..Default::default()
4056 });
4057 let delivered = time::timeout(Duration::from_secs(5), receiver)
4058 .await
4059 .map_or_else(
4060 |_| Err("pending".to_owned()),
4061 |answer| answer.map(drop).map_err(|error| error.to_string()),
4062 );
4063 let actual = ValidatedSend {
4064 delivered,
4065 produce_requests: produce_requests.load(Ordering::SeqCst),
4066 };
4067 mock.stop();
4068 drop(producer);
4069 assert2::assert!(actual == expected, "{name}");
4070 }
4071 }
4072
4073 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4076 async fn debug_names_the_compression_level_of_the_codec() {
4077 let (mock, _) = three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4078 let mut actual = Vec::new();
4079 for compression in [Compression::Gzip, Compression::Zstd, Compression::Snappy] {
4080 let producer = Producer::builder()
4081 .bootstrap(mock.addr.to_string())
4082 .client_id(CLIENT_ID)
4083 .enable_idempotence(false)
4084 .compression(compression)
4085 .compression_gzip_level(5)
4086 .compression_zstd_level(19)
4087 .build()
4088 .await
4089 .expect("producer connects to mock broker");
4090 let debug = format!("{producer:?}");
4091 drop(producer);
4092 actual.push(
4093 debug
4094 .split(", ")
4095 .find(|field| field.starts_with("compression_level"))
4096 .map(str::to_owned),
4097 );
4098 }
4099 mock.stop();
4100 assert2::assert!(
4101 actual
4102 == vec![
4103 Some("compression_level: Some(5)".to_owned()),
4104 Some("compression_level: Some(19)".to_owned()),
4105 Some("compression_level: None".to_owned()),
4106 ]
4107 );
4108 }
4109
4110 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4115 async fn a_record_that_does_not_fit_sends_the_full_batch_to_free_memory() {
4116 let (mock, _) = three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4117 let producer = Producer::builder()
4118 .bootstrap(mock.addr.to_string())
4119 .client_id(CLIENT_ID)
4120 .enable_idempotence(false)
4121 .batch_size(64)
4122 .buffer_memory(125)
4126 .linger(Duration::from_secs(30))
4127 .max_block(Duration::from_secs(2))
4128 .build()
4129 .await
4130 .expect("producer connects to mock broker");
4131 let record = || ProducerRecord {
4132 topic: METADATA_TOPIC.into(),
4133 partition: Some(0),
4134 value: Some(Bytes::from_static(&[0; 40])),
4135 ..Default::default()
4136 };
4137
4138 let first = producer.enqueue(record()).await.expect("first is queued");
4139 let second = producer.enqueue(record()).await.expect("second is queued");
4140 let delivered = time::timeout(Duration::from_secs(5), async {
4141 let first = first.await.map(drop);
4142 let second = producer.flush().await.and(second.await.map(drop));
4143 (
4144 first.map_err(|e| e.to_string()),
4145 second.map_err(|e| e.to_string()),
4146 )
4147 })
4148 .await;
4149 mock.stop();
4150 drop(producer);
4151
4152 assert2::assert!(delivered == Ok((Ok(()), Ok(()))));
4153 }
4154
4155 struct SizeLimits {
4158 max_request_size: Option<usize>,
4159 buffer_memory: usize,
4160 }
4161
4162 #[derive(Debug, PartialEq, Eq)]
4164 struct LargeRecordOutcome {
4165 value_bytes: usize,
4167 delivered: Result<(), String>,
4169 produce_requests: usize,
4171 }
4172
4173 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4180 async fn send_fails_a_record_larger_than_max_request_size_or_buffer_memory() {
4181 const MIB: usize = 1024 * 1024;
4182 let max_request_error = |size: usize, limit: usize| {
4183 format!(
4184 "The message is {size} bytes when serialized which is larger than {limit}, \
4185 which is the value of the max_request_size configuration."
4186 )
4187 };
4188 let buffer_memory_error = |size: usize| {
4189 format!(
4190 "The message is {size} bytes when serialized which is larger than the total \
4191 memory buffer you have configured with the buffer_memory configuration."
4192 )
4193 };
4194 let default_limits = || SizeLimits {
4195 max_request_size: None,
4196 buffer_memory: 32 * MIB,
4197 };
4198 let cases = [
4202 (
4203 "record of max_request_size bytes, default limit",
4204 default_limits(),
4205 1_048_489,
4206 LargeRecordOutcome {
4207 value_bytes: 1_048_489,
4208 delivered: Ok(()),
4209 produce_requests: 1,
4210 },
4211 ),
4212 (
4213 "one byte over max_request_size, default limit",
4214 default_limits(),
4215 1_048_490,
4216 LargeRecordOutcome {
4217 value_bytes: 1_048_490,
4218 delivered: Err(max_request_error(1_048_577, MIB)),
4219 produce_requests: 0,
4220 },
4221 ),
4222 (
4223 "record of buffer_memory bytes",
4224 SizeLimits {
4225 max_request_size: None,
4226 buffer_memory: 1024,
4227 },
4228 938,
4229 LargeRecordOutcome {
4230 value_bytes: 938,
4231 delivered: Ok(()),
4232 produce_requests: 1,
4233 },
4234 ),
4235 (
4236 "one byte over buffer_memory",
4237 SizeLimits {
4238 max_request_size: None,
4239 buffer_memory: 1024,
4240 },
4241 939,
4242 LargeRecordOutcome {
4243 value_bytes: 939,
4244 delivered: Err(buffer_memory_error(1025)),
4245 produce_requests: 0,
4246 },
4247 ),
4248 (
4249 "over both limits reports max_request_size first",
4250 SizeLimits {
4251 max_request_size: Some(500),
4252 buffer_memory: 400,
4253 },
4254 939,
4255 LargeRecordOutcome {
4256 value_bytes: 939,
4257 delivered: Err(max_request_error(1025, 500)),
4258 produce_requests: 0,
4259 },
4260 ),
4261 (
4262 "a larger max_request_size accepts a larger record",
4263 SizeLimits {
4264 max_request_size: Some(2 * MIB),
4265 buffer_memory: 32 * MIB,
4266 },
4267 1_048_490,
4268 LargeRecordOutcome {
4269 value_bytes: 1_048_490,
4270 delivered: Ok(()),
4271 produce_requests: 1,
4272 },
4273 ),
4274 ];
4275
4276 let mut actual = Vec::new();
4277 let mut wanted = Vec::new();
4278 for (name, limits, value_bytes, expected) in cases {
4279 let (mock, produce_count) =
4280 three_partition_broker(Arc::new(AtomicBool::new(true))).await;
4281 let producer = Producer::builder()
4282 .bootstrap(mock.addr.to_string())
4283 .client_id(CLIENT_ID)
4284 .enable_idempotence(false)
4285 .batch_size(64)
4286 .buffer_memory(limits.buffer_memory)
4287 .maybe_max_request_size(limits.max_request_size)
4288 .build()
4289 .await
4290 .expect("producer connects to mock broker");
4291 let delivered = producer
4292 .send(ProducerRecord {
4293 topic: METADATA_TOPIC.into(),
4294 partition: Some(0),
4295 value: Some(Bytes::from(vec![0; value_bytes])),
4296 ..Default::default()
4297 })
4298 .await
4299 .map(drop)
4300 .map_err(|error| error.to_string());
4301 mock.stop();
4302 drop(producer);
4303 actual.push((
4304 name,
4305 LargeRecordOutcome {
4306 value_bytes,
4307 delivered,
4308 produce_requests: produce_count.load(Ordering::SeqCst),
4309 },
4310 ));
4311 wanted.push((name, expected));
4312 }
4313 assert2::assert!(actual == wanted);
4314 }
4315
4316 async fn send_with_full_buffer_memory(
4322 answer_after: Option<Duration>,
4323 max_block: Duration,
4324 ) -> BlockedSendOutcome {
4325 const BATCH_BYTES: usize = 16 * 1024 * 1024;
4326 let answer_produce = Arc::new(AtomicBool::new(false));
4327 let (mock, _) = three_partition_broker(Arc::clone(&answer_produce)).await;
4328 let producer = Producer::builder()
4329 .bootstrap(mock.addr.to_string())
4330 .client_id(CLIENT_ID)
4331 .enable_idempotence(false)
4332 .batch_size(BATCH_BYTES)
4333 .request_timeout(Duration::from_millis(300))
4334 .retry_backoff(Duration::from_millis(20))
4335 .max_block(max_block)
4336 .build()
4337 .await
4338 .expect("producer connects to mock broker");
4339 let pinned = |partition: i32| ProducerRecord {
4340 topic: METADATA_TOPIC.into(),
4341 partition: Some(partition),
4342 value: Some(Bytes::from_static(b"v")),
4343 ..Default::default()
4344 };
4345 let _first = producer.enqueue(pinned(0)).await.expect("first is queued");
4346 let _second = producer.enqueue(pinned(1)).await.expect("second is queued");
4347 if let Some(delay) = answer_after {
4348 tokio::spawn(async move {
4349 time::sleep(delay).await;
4350 answer_produce.store(true, Ordering::SeqCst);
4351 });
4352 }
4353 let started = Instant::now();
4354 let third = producer.enqueue(pinned(2)).await;
4355 let blocked = started.elapsed() >= Duration::from_millis(80);
4356 let delivered = match third {
4357 Err(error) => Err(error.to_string()),
4358 Ok(handle) => time::timeout(Duration::from_secs(3), handle)
4359 .await
4360 .map_or_else(
4361 |_| Err("pending".to_owned()),
4362 |result| result.map(drop).map_err(|error| error.to_string()),
4363 ),
4364 };
4365 mock.stop();
4366 drop(producer);
4367 BlockedSendOutcome { blocked, delivered }
4368 }
4369
4370 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4374 async fn send_waits_for_buffer_memory_up_to_max_block() {
4375 let cases = [
4376 (
4377 "memory stays full",
4378 None,
4379 Duration::from_millis(200),
4380 BlockedSendOutcome {
4381 blocked: true,
4382 delivered: Err(
4383 "Failed to allocate 16777216 bytes within the configured max \
4384 blocking time 200 ms. Total memory: 33554432 bytes. Available \
4385 memory: 0 bytes. Poolable size: 16777216 bytes"
4386 .to_owned(),
4387 ),
4388 },
4389 ),
4390 (
4391 "earlier batches complete",
4392 Some(Duration::from_millis(100)),
4393 Duration::from_secs(5),
4394 BlockedSendOutcome {
4395 blocked: true,
4396 delivered: Ok(()),
4397 },
4398 ),
4399 ];
4400 let mut actual = Vec::new();
4401 let mut wanted = Vec::new();
4402 for (name, answer_after, max_block, expected) in cases {
4403 actual.push((
4404 name,
4405 send_with_full_buffer_memory(answer_after, max_block).await,
4406 ));
4407 wanted.push((name, expected));
4408 }
4409 assert2::assert!(actual == wanted);
4410 }
4411
4412 #[test]
4416 fn only_a_transport_failure_can_have_reached_the_broker() {
4417 use krabka_client_core::ClientError;
4418 use krabka_units::secs;
4419 for (name, error, expected) in [
4420 ("closed connection", ClientError::Disconnected, true),
4421 ("request timeout", ClientError::Timeout(secs(1)), true),
4422 (
4423 "io failure",
4424 ClientError::Io(io::Error::other("broken pipe")),
4425 true,
4426 ),
4427 (
4428 "incompatible version",
4429 ClientError::IncompatibleVersion {
4430 api_key: 25,
4431 broker_min: 4,
4432 broker_max: 5,
4433 client_min: 0,
4434 client_max: 3,
4435 },
4436 false,
4437 ),
4438 (
4439 "no coordinator",
4440 ClientError::NoCoordinator {
4441 key: "group-a".into(),
4442 },
4443 false,
4444 ),
4445 ] {
4446 assert2::assert!(
4447 request_may_have_reached_the_broker(&error) == expected,
4448 "{name}"
4449 );
4450 }
4451 }
4452
4453 async fn leader_broker(leaders: &'static [i32]) -> (MockBroker, Arc<Mutex<Vec<i32>>>) {
4457 let port = Arc::new(AtomicU16::new(0));
4458 let handler_port = Arc::clone(&port);
4459 let produce_log = Arc::new(Mutex::new(Vec::new()));
4460 let handler_produce_log = Arc::clone(&produce_log);
4461 let mock = MockBroker::start(move |api_key, version, _corr_id, body| {
4462 if api_key == api_versions_request::API_KEY {
4463 return Some(encode_v0(&ApiVersionsResponse {
4464 api_keys: vec![
4465 ApiVersion {
4466 api_key: metadata_request::API_KEY,
4467 min_version: 0,
4468 max_version: 12,
4469 ..Default::default()
4470 },
4471 ApiVersion {
4472 api_key: produce_request::API_KEY,
4473 min_version: PRODUCE_VERSION,
4474 max_version: PRODUCE_VERSION,
4475 ..Default::default()
4476 },
4477 ],
4478 ..Default::default()
4479 }));
4480 }
4481 if api_key == metadata_request::API_KEY {
4482 let response = MetadataResponse {
4483 brokers: vec![MetadataResponseBroker {
4484 node_id: 1,
4485 host: "127.0.0.1".into(),
4486 port: i32::from(handler_port.load(Ordering::SeqCst)),
4487 ..Default::default()
4488 }],
4489 topics: vec![MetadataResponseTopic {
4490 name: Some(METADATA_TOPIC.into()),
4491 partitions: (0..)
4492 .zip(leaders)
4493 .map(|(partition_index, &leader_id)| MetadataResponsePartition {
4494 partition_index,
4495 leader_id,
4496 ..Default::default()
4497 })
4498 .collect(),
4499 ..Default::default()
4500 }],
4501 ..Default::default()
4502 };
4503 let mut buf = BytesMut::new();
4504 if version >= metadata_response::FLEXIBLE_MIN {
4505 buf.extend_from_slice(&[0]);
4506 }
4507 response.encode(&mut buf, version).expect("encode Metadata");
4508 return Some(buf.to_vec());
4509 }
4510 if api_key == produce_request::API_KEY {
4511 let mut request_body = &body[2 + CLIENT_ID.len()..];
4512 let request =
4513 ProduceRequest::decode(&mut request_body, version).expect("decode Produce");
4514 handler_produce_log.lock().unwrap().extend(
4515 request
4516 .topic_data
4517 .iter()
4518 .flat_map(|topic| topic.partition_data.iter().map(|p| p.index)),
4519 );
4520 return Some(produce_answer(&request));
4521 }
4522 None
4523 })
4524 .await
4525 .unwrap();
4526 port.store(mock.addr.port(), Ordering::SeqCst);
4527 (mock, produce_log)
4528 }
4529
4530 #[derive(Debug, PartialEq, Eq)]
4532 struct KeylessSpread {
4533 keyless_partitions: usize,
4535 keyless_on_a_partition_without_leader: bool,
4537 keyed_partition: Option<i32>,
4539 produce_requests: usize,
4541 }
4542
4543 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4550 async fn keyless_records_stay_on_one_partition_with_a_leader() {
4551 const KEYED: &[u8] = b"kafka";
4552 let mut actual_rows = Vec::new();
4553 let mut expected_rows = Vec::new();
4554 for (name, leaders, keyed_at, expected) in [
4555 (
4556 "drains do not move the sticky partition",
4557 &[1, 1, 1, 1][..],
4558 None,
4559 KeylessSpread {
4560 keyless_partitions: 1,
4561 keyless_on_a_partition_without_leader: false,
4562 keyed_partition: None,
4563 produce_requests: 6,
4564 },
4565 ),
4566 (
4567 "partitions without a leader are skipped",
4568 &[1, -1, -1, 1][..],
4569 None,
4570 KeylessSpread {
4571 keyless_partitions: 1,
4572 keyless_on_a_partition_without_leader: false,
4573 keyed_partition: None,
4574 produce_requests: 6,
4575 },
4576 ),
4577 (
4578 "a keyed record does not move the sticky partition",
4579 &[1, 1, 1, 1][..],
4580 Some(2),
4581 KeylessSpread {
4582 keyless_partitions: 1,
4583 keyless_on_a_partition_without_leader: false,
4584 keyed_partition: Some(partition_for_key(KEYED, 4)),
4585 produce_requests: 6,
4586 },
4587 ),
4588 ] {
4589 let closed = {
4590 let (mock, produce_log) = leader_broker(leaders).await;
4591 let producer = Producer::builder()
4592 .bootstrap(mock.addr.to_string())
4593 .client_id(CLIENT_ID)
4594 .enable_idempotence(false)
4595 .linger(Duration::ZERO)
4596 .build()
4597 .await
4598 .expect("producer connects to mock broker");
4599 let mut keyless = BTreeSet::new();
4600 let mut keyed_partition = None;
4601 for index in 0..6 {
4602 let key = (keyed_at == Some(index)).then_some(Bytes::from_static(KEYED));
4603 let partition = producer
4604 .send(ProducerRecord {
4605 topic: METADATA_TOPIC.into(),
4606 key: key.clone(),
4607 value: Some(Bytes::from_static(b"value")),
4608 ..Default::default()
4609 })
4610 .await
4611 .expect("the record is delivered")
4612 .partition;
4613 if key.is_some() {
4614 keyed_partition = Some(partition);
4615 } else {
4616 keyless.insert(partition);
4617 }
4618 }
4619 let actual = KeylessSpread {
4620 keyless_partitions: keyless.len(),
4621 keyless_on_a_partition_without_leader: keyless.iter().any(|&partition| {
4622 usize::try_from(partition).is_ok_and(|index| leaders[index] < 0)
4623 }),
4624 keyed_partition,
4625 produce_requests: produce_log.lock().unwrap().len(),
4626 };
4627 producer.close().await.map(|()| {
4628 mock.stop();
4629 actual
4630 })
4631 };
4632 actual_rows.push((name, closed.expect("close producer")));
4633 expected_rows.push((name, expected));
4634 }
4635 assert2::assert!(actual_rows == expected_rows);
4636 }
4637}
4638
4639#[cfg(test)]
4640mod transport_configuration_tests {
4641 use std::time::Duration;
4642
4643 use assert2::assert;
4644 use bytes::BytesMut;
4645 use krabka_client_core::{ClientDnsLookup, MockBroker};
4646 use krabka_protocol::{
4647 Encode,
4648 owned::{api_versions_request, api_versions_response::ApiVersionsResponse},
4649 };
4650 use krabka_units::{millis, secs};
4651
4652 use super::{Acks, Producer, ProducerError};
4653
4654 #[tokio::test]
4655 async fn producer_retains_transport_overrides_in_primary_and_coordinator_clients()
4656 -> Result<(), ProducerError> {
4657 let mock = MockBroker::start(|api, _, _, _| {
4658 if api != api_versions_request::API_KEY {
4659 return None;
4660 }
4661 let mut body = BytesMut::new();
4662 ApiVersionsResponse::default()
4663 .encode(&mut body, 0)
4664 .expect("encode ApiVersions");
4665 Some(body.to_vec())
4666 })
4667 .await
4668 .unwrap();
4669 let producer = Producer::builder()
4670 .bootstrap(mock.addr.to_string())
4671 .request_timeout(Duration::from_millis(10))
4672 .acks(Acks::Zero)
4673 .enable_idempotence(false)
4674 .enable_metrics_push(false)
4675 .dns_timeout(millis(95))
4676 .client_dns_lookup(ClientDnsLookup::UseAllDnsIps)
4677 .reconnect_backoff(millis(37))
4678 .reconnect_backoff_max(millis(137))
4679 .connections_max_idle(secs(67))
4680 .socket_connection_setup_timeout(secs(17))
4681 .socket_connection_setup_timeout_max(secs(27))
4682 .build()
4683 .await
4684 .expect("producer");
4685 let coordinator = producer
4686 .connect_txn_coordinator(mock.addr.to_string())
4687 .await
4688 .expect("coordinator");
4689 for options in [
4690 producer.client.connection_options(),
4691 coordinator.connection_options(),
4692 ] {
4693 assert!(
4694 (
4695 options.dns_timeout.time(),
4696 options.client_dns_lookup,
4697 options.reconnect_backoff,
4698 options.reconnect_backoff_max,
4699 options.connections_max_idle,
4700 options.socket_connection_setup_timeout,
4701 options.socket_connection_setup_timeout_max
4702 ) == (
4703 millis(95),
4704 ClientDnsLookup::UseAllDnsIps,
4705 millis(37),
4706 millis(137),
4707 secs(67),
4708 secs(17),
4709 secs(27)
4710 )
4711 );
4712 }
4713 assert!(producer.client.refresh_metadata().await.is_err());
4714 coordinator.close();
4715 producer.close().await.map(|()| mock.stop())
4716 }
4717}