mod accumulator;
mod batch;
mod config;
mod engine;
mod gate;
mod identity;
mod partitioner;
mod record;
mod retry;
mod transaction;
mod typed;
pub use accumulator::DeliveryHandle;
pub use config::Acks;
pub(crate) use config::ProducerConfig;
pub use partitioner::{Partitioner, RoundRobinPartitioner, murmur2};
pub use record::{DeliveryConfirmation, Record, RecordMetadata, UNKNOWN_PARTITION};
pub use transaction::{
PreparedTxnState, TopicPartitionOffset, TransactionOutcome, TransactionState,
TransactionVersion, TransactionalProducer,
};
pub use typed::TypedProducer;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::Instant;
use bytes::Bytes;
use tracing::{debug, info, warn};
use crate::PartitionId;
use crate::client::{CloseOptions, Kafka};
use crate::error::{KrafkaError, ProtocolErrorKind, Result};
use crate::metadata::ClusterMetadata;
use crate::metrics::{ClientInstanceId, Metrics, MetricsSource, ProducerRecorder};
use crate::protocol::{ApiKey, Compression, ProduceRequest, ProduceResponse, VersionedEncode};
use crate::telemetry::{ClientType, Telemetry};
use accumulator::Accumulator;
use gate::TxnGate;
use partitioner::Partitioning;
use record::TopicHandle;
use crate::barrier::InFlightBarrier;
pub(crate) async fn resolve_partition(
metadata: &ClusterMetadata,
partitioning: &Partitioning,
topic: &TopicHandle,
key: Option<&[u8]>,
requested: Option<PartitionId>,
record_size: usize,
max_wait: Duration,
) -> Result<PartitionId> {
let partition_count = metadata.ensure_partition_count(topic, max_wait).await?;
let (partition, source) = match requested {
Some(partition) => (partition, "invalid partition given with record"),
None => (
partitioning.partition(metadata, topic, key, record_size, partition_count),
"the partitioner returned partition",
),
};
if partition < 0 || partition as usize >= partition_count {
return Err(KrafkaError::config(format!(
"{source} {partition}, which is not in the range [0, {partition_count}) for topic \
{topic}"
)));
}
Ok(partition)
}
pub(crate) struct SendObligation<'a> {
interceptor: &'a dyn crate::interceptor::ProducerInterceptor,
topic: TopicHandle,
pub(crate) context: Option<crate::interceptor::RecordContext>,
suspended: bool,
pub(crate) span: tracing::Span,
}
impl<'a> SendObligation<'a> {
fn on_send(
interceptor: &'a dyn crate::interceptor::ProducerInterceptor,
record: &mut Record,
client_id: &str,
) -> std::result::Result<Self, KrafkaError> {
let mut context = crate::interceptor::RecordContext::new();
let outcome = crate::interceptor::safe_on_send(interceptor, record, &mut context);
let mut obligation = Self {
interceptor,
topic: TopicHandle::from(record.topic.as_str()),
context: Some(context),
suspended: false,
span: crate::tracing_ext::send_span(
&record.topic,
client_id,
record.key.as_deref(),
record.value.is_none(),
),
};
match outcome {
Ok(()) => Ok(obligation),
Err(error) => Err(obligation.fail(UNKNOWN_PARTITION, &record.headers, error)),
}
}
pub(crate) fn topic(&self) -> TopicHandle {
TopicHandle::clone(&self.topic)
}
pub(crate) async fn suspend<T>(&mut self, fut: impl std::future::Future<Output = T>) -> T {
self.suspended = true;
let output = fut.await;
self.suspended = false;
output
}
pub(crate) fn fail(
&mut self,
partition: PartitionId,
headers: &crate::Headers,
error: KrafkaError,
) -> KrafkaError {
crate::tracing_ext::record_error(&self.span, &error);
if let Some(mut context) = self.context.take() {
crate::interceptor::safe_on_acknowledgement(
self.interceptor,
&self.topic,
partition,
Err(&error),
headers,
&mut context,
);
}
error
}
pub(crate) fn take_context(&mut self) -> crate::interceptor::RecordContext {
self.context.take().unwrap_or_default()
}
}
impl Drop for SendObligation<'_> {
fn drop(&mut self) {
let (Some(mut context), false) = (self.context.take(), std::thread::panicking()) else {
return;
};
let error = if self.suspended {
KrafkaError::closed("the send was cancelled before the record was queued")
} else {
debug_assert!(
false,
"krafka bug: a record ran on_send but no on_acknowledgement was reported",
);
tracing::error!(
"krafka bug: a record ran on_send but no on_acknowledgement was reported",
);
KrafkaError::illegal_state(
"krafka bug: a record ran on_send but no on_acknowledgement was reported",
)
};
crate::tracing_ext::record_error(&self.span, &error);
crate::interceptor::safe_on_acknowledgement(
self.interceptor,
&self.topic,
UNKNOWN_PARTITION,
Err(&error),
&Vec::new(),
&mut context,
);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn enqueue_record<R, F>(
accumulator: &Accumulator,
metadata: &ClusterMetadata,
partitioning: &Partitioning,
interceptor: &dyn crate::interceptor::ProducerInterceptor,
client_id: &str,
max_block: Duration,
record: Record,
register: R,
admit: impl FnOnce() -> Result<Option<gate::TxnTicket>>,
) -> Result<DeliveryHandle>
where
R: FnOnce(TopicHandle, PartitionId) -> F,
F: std::future::Future<Output = Result<()>>,
{
let send_started_at = Instant::now();
let mut record = record;
let mut obligation = SendObligation::on_send(interceptor, &mut record, client_id)?;
if let Err(error) = record.validate() {
return Err(obligation.fail(UNKNOWN_PARTITION, &record.headers, error));
}
let record_size = record.estimated_size();
let routed = record.into_routed_parts_with_topic(obligation.topic());
let topic = routed.topic;
let record = routed.record;
let partition = match obligation
.suspend(resolve_partition(
metadata,
partitioning,
&topic,
record.key_bytes(),
routed.partition,
record_size,
max_block.saturating_sub(send_started_at.elapsed()),
))
.await
{
Ok(partition) => partition,
Err(error) => return Err(obligation.fail(UNKNOWN_PARTITION, &record.headers, error)),
};
obligation.span.record(
"messaging.destination.partition.id",
tracing::field::display(partition),
);
if let Err(error) = obligation
.suspend(register(TopicHandle::clone(&topic), partition))
.await
{
return Err(obligation.fail(partition, &record.headers, error));
}
match accumulator
.enqueue(
topic,
record,
record_size,
partition,
send_started_at,
&mut obligation,
admit,
)
.await
{
Ok(handle) => Ok(handle),
Err(rejected) => {
obligation.context = Some(rejected.context);
Err(obligation.fail(partition, &rejected.record.headers, rejected.error))
}
}
}
pub struct Producer {
kafka: Kafka,
config: ProducerConfig,
partitioning: Arc<Partitioning>,
accumulator: Accumulator,
barrier: Arc<InFlightBarrier>,
metrics_source: Arc<MetricsSource>,
telemetry: Telemetry,
interceptor: Arc<dyn crate::interceptor::ProducerInterceptor>,
}
fn apply_produce_leader_hint(
metadata: &ClusterMetadata,
topic: &str,
partition: PartitionId,
response: &ProduceResponse,
partition_response: &crate::protocol::ProducePartitionResponse,
) -> bool {
use crate::error::ErrorCode;
if !matches!(
partition_response.error_code,
ErrorCode::NotLeaderForPartition | ErrorCode::FencedLeaderEpoch
) {
return false;
}
let Some(leader) = partition_response.current_leader else {
return false;
};
let applied = metadata.apply_leader_hint(
topic,
partition,
leader.leader_id,
leader.leader_epoch,
crate::metadata::broker_info_for_node(&response.node_endpoints, leader.leader_id),
);
if applied {
debug!(
topic,
partition,
leader_id = leader.leader_id,
leader_epoch = leader.leader_epoch,
"broker named a new leader; retrying there without a metadata refresh (KIP-951)"
);
}
applied
}
fn request_header_size(api_key: ApiKey, api_version: i16, client_id: &str) -> Result<usize> {
if client_id.len() > i16::MAX as usize {
return Err(KrafkaError::protocol_kind(
ProtocolErrorKind::InvalidLength,
format!(
"client_id length {} exceeds protocol limit of {}",
client_id.len(),
i16::MAX
),
));
}
let base = 2 + 2 + 4 + 2 + client_id.len();
match crate::protocol::RequestHeader::header_version(api_key, api_version) {
1 => Ok(base),
2 => Ok(base + 1), version => Err(KrafkaError::protocol_kind(
ProtocolErrorKind::UnknownApiVersion,
format!("unsupported request header version {version}"),
)),
}
}
fn encode_and_validate_produce_request(
client_id: &str,
max_request_size: usize,
api_version: i16,
request: &ProduceRequest,
) -> Result<Bytes> {
let mut body = bytes::BytesMut::new();
request.encode_versioned(api_version, &mut body)?;
let frame_size = 4 + request_header_size(ApiKey::Produce, api_version, client_id)? + body.len();
if frame_size > max_request_size {
return Err(KrafkaError::protocol_kind(
ProtocolErrorKind::FrameTooLarge,
format!(
"produce request size {frame_size} exceeds max_request_size {max_request_size}"
),
));
}
Ok(body.freeze())
}
pub(crate) fn fill_produce_topic_ids(
request: &mut ProduceRequest,
metadata: &ClusterMetadata,
) -> bool {
let mut all_resolved = true;
for topic_data in &mut request.topic_data {
if topic_data.topic_id.is_none() {
if let Some(id) = metadata.topic_id_for_name(&topic_data.name) {
topic_data.topic_id = Some(id);
} else {
all_resolved = false;
}
}
}
all_resolved
}
impl std::fmt::Debug for Producer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Producer")
.field("client_id", &self.kafka.client_id())
.field("idempotent", &self.config.idempotent)
.finish_non_exhaustive()
}
}
pub(crate) fn interceptor_chain(
mut interceptors: Vec<Arc<dyn crate::interceptor::ProducerInterceptor>>,
) -> Arc<dyn crate::interceptor::ProducerInterceptor> {
match interceptors.len() {
0 => Arc::new(crate::interceptor::NoOpProducerInterceptor),
1 => interceptors
.pop()
.unwrap_or_else(|| Arc::new(crate::interceptor::NoOpProducerInterceptor)),
_ => Arc::new(crate::interceptor::ProducerInterceptorChain::new(
interceptors,
)),
}
}
pub(crate) struct Transactional {
pub(crate) transactional_id: String,
pub(crate) gate: Arc<TxnGate>,
}
impl Producer {
async fn new(
kafka: Kafka,
config: ProducerConfig,
interceptor: Arc<dyn crate::interceptor::ProducerInterceptor>,
partitioner: Option<Arc<dyn Partitioner>>,
transactional: Option<Transactional>,
) -> Result<Self> {
let backoff = retry::Backoff::new(config.retry_backoff);
let identity = if config.idempotent && transactional.is_none() {
let deadline = tokio::time::Instant::now() + config.delivery_timeout;
Some(engine::init_producer_id(kafka.metadata(), &backoff, deadline).await?)
} else {
None
};
let partitioning = Arc::new(Partitioning::new(
partitioner,
config.batch_size,
config
.partitioner_rack_aware
.then(|| config.client_rack.clone())
.flatten(),
));
let metrics = Arc::new(ProducerRecorder::default());
let metrics_source = MetricsSource::producer(&kafka, Arc::clone(&metrics));
let barrier = Arc::new(InFlightBarrier::new());
let (transactional_id, gate, mode) = match transactional {
Some(txn) => (
Some(txn.transactional_id),
Some(txn.gate),
identity::Mode::Transactional { tv2: false },
),
None if config.idempotent => (None, None, identity::Mode::Idempotent),
None => (None, None, identity::Mode::Plain),
};
let accumulator = Accumulator::spawn(
engine::EngineConfig {
batch_size: config.batch_size,
linger: config.linger,
delivery_timeout: config.delivery_timeout,
request_timeout: kafka.request_timeout(),
max_request_size: config.max_request_size,
acks: config.acks.to_i16(),
compression: config.compression,
compression_level: config.compression_level,
topic_compression: config.topic_compression.clone().into_iter().collect(),
client_id: kafka.client_id().to_string(),
transactional_id,
backoff,
interceptor: Arc::clone(&interceptor),
mode,
identity,
gate,
},
config.buffer_memory,
config.max_block,
Arc::clone(kafka.metadata()),
Arc::clone(&metrics),
Arc::clone(&barrier),
);
info!(client_id = kafka.client_id(), "producer started");
let telemetry = Telemetry::start(
config.metrics_push,
&kafka,
ClientType::Producer,
Arc::clone(&metrics_source),
);
Ok(Self {
kafka,
config,
partitioning,
accumulator,
barrier,
metrics_source,
telemetry,
interceptor,
})
}
pub async fn send(&self, record: Record) -> Result<RecordMetadata> {
self.enqueue(record).await?.await
}
pub async fn enqueue(&self, record: Record) -> Result<DeliveryHandle> {
enqueue_record(
&self.accumulator,
self.kafka.metadata(),
&self.partitioning,
&*self.interceptor,
self.kafka.client_id(),
self.config.max_block,
record,
|_, _| async { Ok(()) },
|| Ok(None),
)
.await
}
pub async fn partitions_for(&self, topic: &str) -> Result<Vec<crate::PartitionInfo>> {
let metadata = self.kafka.metadata();
metadata
.ensure_partition_count(topic, self.config.max_block)
.await?;
let mut partitions: Vec<_> = metadata
.topic_arc(topic)
.map(|info| info.partitions_iter().cloned().collect())
.unwrap_or_default();
partitions.sort_by_key(|p| p.partition);
Ok(partitions)
}
pub async fn flush(&self) -> Result<()> {
let generation = self.barrier.snapshot();
self.accumulator.flush(generation);
self.barrier.wait_for(generation).await;
Ok(())
}
pub async fn close(&self) -> Result<()> {
self.close_with(CloseOptions::new()).await
}
pub async fn close_with(&self, options: CloseOptions) -> Result<()> {
let Some(generation) = self.barrier.begin_close() else {
return Ok(());
};
self.finish_close(generation, options.timeout).await
}
async fn finish_close(&self, generation: u64, timeout: Option<Duration>) -> Result<()> {
let started = Instant::now();
self.accumulator.flush(generation);
let drained = self.barrier.wait_for(generation);
let result = match timeout {
Some(timeout) => tokio::time::timeout(timeout, drained).await.map_err(|_| {
warn!("producer close timed out; failing the records still queued");
self.accumulator
.terminate(KrafkaError::closed("the producer was closed"));
KrafkaError::timeout("producer close")
}),
None => {
drained.await;
Ok(())
}
};
crate::interceptor::safe_producer_close(&*self.interceptor);
let telemetry_budget = timeout.map_or(self.kafka.request_timeout(), |t| {
t.saturating_sub(started.elapsed())
});
self.telemetry.close(telemetry_budget).await;
info!("producer closed");
result
}
#[inline]
pub fn is_closed(&self) -> bool {
self.barrier.is_closing()
}
pub fn metrics(&self) -> Metrics {
self.metrics_source.snapshot()
}
pub async fn client_instance_id(&self, timeout: Duration) -> Result<Option<ClientInstanceId>> {
self.telemetry.client_instance_id(timeout).await
}
}
impl Drop for Producer {
fn drop(&mut self) {
if !self.barrier.is_closing() && !std::thread::panicking() {
warn!(
"Producer dropped without close(); buffered records are still being sent in \
the background with nobody waiting for them. Call `close()` before drop."
);
crate::interceptor::safe_producer_close(&*self.interceptor);
}
}
}
#[must_use = "builders do nothing until .build() is called"]
pub struct ProducerBuilder {
kafka: Kafka,
config: ProducerConfig,
interceptors: Vec<Arc<dyn crate::interceptor::ProducerInterceptor>>,
partitioner: Option<Arc<dyn Partitioner>>,
}
impl std::fmt::Debug for ProducerBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProducerBuilder")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl ProducerBuilder {
pub(crate) fn new(kafka: Kafka) -> Self {
Self {
kafka,
config: ProducerConfig::default(),
interceptors: Vec::new(),
partitioner: None,
}
}
pub fn acks(mut self, acks: Acks) -> Self {
self.config.acks = acks;
self
}
pub fn compression(mut self, compression: Compression) -> Self {
self.config.compression = compression;
self
}
pub fn compression_level(mut self, level: Option<i32>) -> Self {
self.config.compression_level = level;
self
}
pub fn topic_compression(mut self, topic: impl Into<String>, compression: Compression) -> Self {
self.config
.topic_compression
.insert(topic.into(), compression);
self
}
pub fn batch_size(mut self, bytes: usize) -> Self {
self.config.batch_size = bytes;
self
}
pub fn linger(mut self, linger: Duration) -> Self {
self.config.linger = linger;
self
}
pub fn delivery_timeout(mut self, timeout: Duration) -> Self {
self.config.delivery_timeout = timeout;
self
}
pub fn retry_backoff(mut self, backoff: Duration) -> Self {
self.config.retry_backoff = backoff;
self
}
pub fn max_request_size(mut self, bytes: usize) -> Self {
self.config.max_request_size = bytes;
self
}
pub fn idempotent(mut self, enable: bool) -> Self {
self.config.idempotent = enable;
self
}
pub fn max_block(mut self, duration: Duration) -> Self {
self.config.max_block = duration;
self
}
pub fn buffer_memory(mut self, bytes: usize) -> Self {
self.config.buffer_memory = bytes;
self
}
pub fn client_rack(mut self, rack: impl Into<String>) -> Self {
self.config.client_rack = Some(rack.into());
self
}
pub fn partitioner_rack_aware(mut self, enable: bool) -> Self {
self.config.partitioner_rack_aware = enable;
self
}
pub fn partitioner(mut self, partitioner: impl Partitioner + 'static) -> Self {
self.partitioner = Some(Arc::new(partitioner));
self
}
pub fn interceptor(
mut self,
interceptor: impl crate::interceptor::ProducerInterceptor + 'static,
) -> Self {
self.interceptors.push(Arc::new(interceptor));
self
}
pub fn transaction_timeout(mut self, timeout: Duration) -> Self {
self.config.transaction_timeout = Some(timeout);
self
}
pub fn two_phase_commit(mut self, enable: bool) -> Self {
self.config.two_phase_commit = enable;
self
}
pub fn metrics_push(mut self, enable: bool) -> Self {
self.config.metrics_push = enable;
self
}
fn validate(&self, transactional: bool) -> Result<()> {
config::validate(&self.config, self.kafka.request_timeout(), transactional)?;
config::validate_partitioning(
self.config.partitioner_rack_aware,
self.config.client_rack.as_deref(),
self.partitioner.is_some(),
)
}
#[cfg(test)]
pub(crate) fn build_config(self) -> Result<ProducerConfig> {
self.validate(false)?;
Ok(self.config)
}
pub async fn build(self) -> Result<Producer> {
self.validate(false)?;
let interceptor = interceptor_chain(self.interceptors);
Producer::new(self.kafka, self.config, interceptor, self.partitioner, None).await
}
pub async fn build_transactional(
self,
transactional_id: impl Into<String>,
) -> Result<TransactionalProducer> {
let transactional_id = transactional_id.into();
if transactional_id.is_empty() {
return Err(KrafkaError::config("transactional_id must not be empty"));
}
if transactional_id.len() > i16::MAX as usize {
return Err(KrafkaError::config(format!(
"transactional_id is {} bytes, exceeding the Kafka wire limit of {}",
transactional_id.len(),
i16::MAX
)));
}
self.validate(true)?;
let gate = Arc::new(TxnGate::new());
let interceptor = interceptor_chain(self.interceptors);
let producer = Producer::new(
self.kafka,
self.config,
interceptor,
self.partitioner,
Some(Transactional {
transactional_id: transactional_id.clone(),
gate: Arc::clone(&gate),
}),
)
.await?;
TransactionalProducer::start(producer, transactional_id, gate).await
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::protocol::{ProducePartitionData, ProduceTopicData, versions};
#[test]
fn test_validate_produce_request_size_rejects_oversized_frame() {
let request = ProduceRequest {
transactional_id: None,
acks: Acks::All.to_i16(),
timeout_ms: 30_000,
topic_data: vec![ProduceTopicData {
name: "topic".to_string(),
topic_id: None,
partition_data: vec![ProducePartitionData {
index: 0,
records: Bytes::from(vec![0; 512]),
}],
}],
};
let error =
encode_and_validate_produce_request("client", 128, versions::PRODUCE_MIN, &request)
.expect_err("oversized frame should be rejected");
assert!(error.to_string().contains("max_request_size"));
}
#[test]
fn test_validate_produce_request_size_uses_exact_flexible_encoding_size() {
let request = ProduceRequest {
transactional_id: Some("txn-123".to_string()),
acks: Acks::All.to_i16(),
timeout_ms: 30_000,
topic_data: vec![ProduceTopicData {
name: "topic".to_string(),
topic_id: Some([0u8; 16]),
partition_data: vec![ProducePartitionData {
index: 0,
records: Bytes::from(vec![1; 32]),
}],
}],
};
let encoded = encode_and_validate_produce_request(
"client",
usize::MAX,
versions::PRODUCE_MAX,
&request,
)
.unwrap();
let exact_size = 4
+ request_header_size(ApiKey::Produce, versions::PRODUCE_MAX, "client").unwrap()
+ encoded.len();
encode_and_validate_produce_request("client", exact_size, versions::PRODUCE_MAX, &request)
.unwrap();
let error = encode_and_validate_produce_request(
"client",
exact_size.saturating_sub(1),
versions::PRODUCE_MAX,
&request,
)
.unwrap_err();
assert!(error.to_string().contains("max_request_size"));
}
#[test]
fn test_validate_produce_request_size_v13_requires_topic_id() {
let request = ProduceRequest {
transactional_id: None,
acks: Acks::All.to_i16(),
timeout_ms: 30_000,
topic_data: vec![ProduceTopicData {
name: "topic".to_string(),
topic_id: None,
partition_data: vec![ProducePartitionData {
index: 0,
records: Bytes::from_static(b"payload"),
}],
}],
};
let error = encode_and_validate_produce_request("client", 1024, 13, &request).unwrap_err();
assert!(error.to_string().contains("topic_id is required"));
}
#[test]
fn a_record_larger_than_buffer_memory_is_rejected_up_front() {
let err = accumulator::check_record_admission(1024, 16, usize::MAX)
.expect_err("a record larger than buffer_memory must be rejected");
assert!(
err.to_string().contains("buffer_memory"),
"the error must name the setting to raise, got: {err}"
);
}
#[test]
fn test_producer_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Producer>();
}
#[test]
fn test_transaction_version_is_publicly_nameable() {
let version: crate::producer::TransactionVersion = TransactionVersion::V2;
assert_eq!(version, TransactionVersion::V2);
}
#[tokio::test]
async fn interceptors_append_in_order() {
use crate::interceptor::ProducerInterceptor;
#[derive(Debug)]
struct A;
impl ProducerInterceptor for A {}
let builder = crate::Kafka::detached()
.producer()
.interceptor(A)
.interceptor(Arc::new(A));
assert_eq!(builder.interceptors.len(), 2);
}
}