use std::collections::{BTreeMap, VecDeque};
use std::time::Duration;
use barnabas_core::producer::{ProducerIdentity, ProducerState, SequenceRange};
use barnabas_core::{Disposition, ErrorCode, Partitioner};
use bytes::{Bytes, BytesMut};
use kafka_protocol::messages::{
add_offsets_to_txn_request::AddOffsetsToTxnRequest,
add_partitions_to_txn_request::AddPartitionsToTxnTopic,
produce_request::{PartitionProduceData, TopicProduceData},
txn_offset_commit_request::{TxnOffsetCommitRequestPartition, TxnOffsetCommitRequestTopic},
AddOffsetsToTxnResponse, AddPartitionsToTxnRequest, AddPartitionsToTxnResponse, ApiKey,
EndTxnRequest, EndTxnResponse, FindCoordinatorRequest, FindCoordinatorResponse, GroupId,
InitProducerIdRequest, InitProducerIdResponse, ProduceRequest, ProduceResponse, ProducerId,
TopicName, TransactionalId, TxnOffsetCommitRequest, TxnOffsetCommitResponse,
};
use kafka_protocol::protocol::StrBytes;
use kafka_protocol::records::{
Compression, Record, RecordBatchEncoder, RecordEncodeOptions, TimestampType,
};
use crate::cluster::Cluster;
use crate::{Error, Result, Transport};
const MAX_RETRIES: usize = 40;
const RETRY_BACKOFF: Duration = Duration::from_millis(5);
const RETRY_BACKOFF_MAX: Duration = Duration::from_millis(250);
pub const DEFAULT_MAX_IN_FLIGHT: usize = 5;
pub const DEFAULT_BATCH_SIZE: usize = 16 * 1024;
const RECORD_OVERHEAD: usize = 16;
struct Staged {
records: Vec<ProducerRecord>,
bytes: usize,
since: std::time::Instant,
}
type RoundTarget = (String, Vec<(String, i32)>);
type Window = Vec<Vec<RoundTarget>>;
#[derive(Debug, Clone)]
pub struct ProducerRecord {
pub key: Option<Bytes>,
pub value: Option<Bytes>,
pub timestamp: Option<i64>,
}
impl ProducerRecord {
#[must_use]
pub fn new(key: Option<Bytes>, value: Option<Bytes>) -> Self {
Self {
key,
value,
timestamp: None,
}
}
}
pub struct Producer<T: Transport> {
cluster: Cluster<T>,
state: ProducerState,
transactional_id: Option<String>,
coordinator: Option<String>,
acks: i16,
timeout_ms: i32,
compression: Compression,
partitioner: Partitioner,
round_robin: u32,
pending: BTreeMap<(String, i32), VecDeque<Bytes>>,
max_in_flight: usize,
staged: BTreeMap<(String, i32), Staged>,
linger: Duration,
batch_size: usize,
offsets: BTreeMap<((String, i32), usize), i64>,
}
impl<T: Transport> Producer<T> {
pub fn builder(transport: T) -> crate::builder::ProducerBuilder<T> {
crate::builder::ProducerBuilder::new(transport)
}
pub async fn transactional(
transport: T,
bootstrap: &[String],
client_id: &str,
transactional_id: &str,
) -> Result<Self> {
let mut me = Self {
cluster: Cluster::connect(transport, bootstrap, client_id).await?,
state: ProducerState::transactional(),
transactional_id: Some(transactional_id.to_owned()),
coordinator: None,
acks: -1,
timeout_ms: 30_000,
compression: Compression::None,
partitioner: Partitioner::default(),
round_robin: 0,
staged: BTreeMap::new(),
linger: Duration::ZERO,
batch_size: DEFAULT_BATCH_SIZE,
pending: BTreeMap::new(),
max_in_flight: DEFAULT_MAX_IN_FLIGHT,
offsets: BTreeMap::new(),
};
me.init_producer_id().await?;
Ok(me)
}
pub async fn idempotent(transport: T, bootstrap: &[String], client_id: &str) -> Result<Self> {
let mut me = Self {
cluster: Cluster::connect(transport, bootstrap, client_id).await?,
state: ProducerState::idempotent(),
transactional_id: None,
coordinator: None,
acks: -1,
timeout_ms: 30_000,
compression: Compression::None,
partitioner: Partitioner::default(),
round_robin: 0,
staged: BTreeMap::new(),
linger: Duration::ZERO,
batch_size: DEFAULT_BATCH_SIZE,
pending: BTreeMap::new(),
max_in_flight: DEFAULT_MAX_IN_FLIGHT,
offsets: BTreeMap::new(),
};
me.init_producer_id().await?;
Ok(me)
}
pub fn set_partitioner(&mut self, partitioner: Partitioner) {
self.partitioner = partitioner;
}
pub fn set_compression(&mut self, compression: Compression) {
self.compression = compression;
}
#[must_use]
pub fn identity(&self) -> Option<ProducerIdentity> {
self.state.identity()
}
#[must_use]
pub fn state(&self) -> barnabas_core::TxnState {
self.state.state()
}
async fn find_coordinator(&mut self) -> Result<String> {
let Some(txn_id) = self.transactional_id.clone() else {
return Ok(String::new());
};
let mut req = FindCoordinatorRequest::default();
req.key = StrBytes::from_string(txn_id.clone());
req.key_type = 1;
for attempt in 0..MAX_RETRIES {
let resp: FindCoordinatorResponse = self
.cluster
.call_any(ApiKey::FindCoordinator, 3, &req)
.await?;
let code = ErrorCode(resp.error_code);
if code.is_ok() {
let addr = format!("{}:{}", resp.host.as_str(), resp.port);
self.coordinator = Some(addr.clone());
return Ok(addr);
}
match code.disposition() {
Disposition::Retry | Disposition::FindCoordinator => {
Self::backoff(attempt).await;
}
_ => {
return Err(Error::Broker {
op: "FindCoordinator",
code: code.0,
disposition: code.disposition(),
})
}
}
}
Err(Error::Broker {
op: "FindCoordinator",
code: ErrorCode::COORDINATOR_NOT_AVAILABLE.0,
disposition: Disposition::Retry,
})
}
async fn coordinator_addr(&mut self) -> Result<String> {
match self.coordinator.clone() {
Some(addr) => Ok(addr),
None => self.find_coordinator().await,
}
}
async fn backoff(attempt: usize) {
let shift = u32::try_from(attempt).unwrap_or(u32::MAX).min(16);
let delay = RETRY_BACKOFF
.saturating_mul(1u32 << shift)
.min(RETRY_BACKOFF_MAX);
T::sleep(delay).await;
}
async fn coordinator_call<Req, Resp, F>(
&mut self,
op: &'static str,
api_key: ApiKey,
version: i16,
req: &Req,
error_of: F,
) -> Result<Resp>
where
Req: kafka_protocol::protocol::Encodable,
Resp: kafka_protocol::protocol::Decodable,
F: Fn(&Resp) -> i16,
{
for attempt in 0..MAX_RETRIES {
let addr = self.coordinator_addr().await?;
let resp: Resp = if addr.is_empty() {
self.cluster.call_any(api_key, version, req).await?
} else {
self.cluster.call_at(&addr, api_key, version, req).await?
};
let code = ErrorCode(error_of(&resp));
if code.is_ok() {
return Ok(resp);
}
match code.disposition() {
Disposition::Retry => Self::backoff(attempt).await,
Disposition::FindCoordinator => {
self.coordinator = None;
Self::backoff(attempt).await;
}
Disposition::Fatal => {
self.state.fence();
return Err(Error::Broker {
op,
code: code.0,
disposition: Disposition::Fatal,
});
}
Disposition::RefreshMetadata | Disposition::Ok => {
return Err(Error::Broker {
op,
code: code.0,
disposition: code.disposition(),
})
}
}
}
Err(Error::Broker {
op,
code: ErrorCode::COORDINATOR_NOT_AVAILABLE.0,
disposition: Disposition::Retry,
})
}
async fn init_producer_id(&mut self) -> Result<()> {
let mut req = InitProducerIdRequest::default();
req.transactional_id = self
.transactional_id
.as_ref()
.map(|id| TransactionalId(StrBytes::from_string(id.clone())));
req.transaction_timeout_ms = self.timeout_ms;
req.producer_id = ProducerId(-1);
req.producer_epoch = -1;
let resp: InitProducerIdResponse = self
.coordinator_call(
"InitProducerId",
ApiKey::InitProducerId,
4,
&req,
|r: &InitProducerIdResponse| r.error_code,
)
.await?;
self.state.on_init_producer_id(ProducerIdentity {
id: resp.producer_id.0,
epoch: resp.producer_epoch,
});
Ok(())
}
pub fn begin_transaction(&mut self) -> Result<()> {
self.state.begin_transaction().map_err(Error::Producer)
}
pub async fn commit_transaction(&mut self) -> Result<()> {
self.end_transaction(true).await
}
pub async fn abort_transaction(&mut self) -> Result<()> {
self.end_transaction(false).await
}
async fn end_transaction(&mut self, committed: bool) -> Result<()> {
self.state.end_transaction().map_err(Error::Producer)?;
let identity = self.state.identity().ok_or(Error::Missing("producer id"))?;
let txn_id = self
.transactional_id
.clone()
.ok_or(Error::Missing("transactional id"))?;
let mut req = EndTxnRequest::default();
req.transactional_id = TransactionalId(StrBytes::from_string(txn_id));
req.producer_id = ProducerId(identity.id);
req.producer_epoch = identity.epoch;
req.committed = committed;
let _: EndTxnResponse = self
.coordinator_call("EndTxn", ApiKey::EndTxn, 3, &req, |r: &EndTxnResponse| {
r.error_code
})
.await?;
self.state.on_end_transaction();
Ok(())
}
pub async fn send_offsets_to_transaction(
&mut self,
offsets: &BTreeMap<barnabas_core::group::TopicPartition, i64>,
metadata: &crate::group::GroupMetadata,
) -> Result<()> {
if self.state.state() != barnabas_core::TxnState::InTransaction {
return Err(Error::Producer(
barnabas_core::producer::ProducerError::NoTransaction,
));
}
if offsets.is_empty() {
return Ok(());
}
let identity = self.state.identity().ok_or(Error::Missing("producer id"))?;
let txn_id = self
.transactional_id
.clone()
.ok_or(Error::Missing("transactional id"))?;
let mut add = AddOffsetsToTxnRequest::default();
add.transactional_id = TransactionalId(StrBytes::from_string(txn_id.clone()));
add.producer_id = ProducerId(identity.id);
add.producer_epoch = identity.epoch;
add.group_id = GroupId(StrBytes::from_string(metadata.group_id.clone()));
let _: AddOffsetsToTxnResponse = self
.coordinator_call(
"AddOffsetsToTxn",
ApiKey::AddOffsetsToTxn,
3,
&add,
|r: &AddOffsetsToTxnResponse| r.error_code,
)
.await?;
let mut topics: BTreeMap<String, Vec<TxnOffsetCommitRequestPartition>> = BTreeMap::new();
for (tp, offset) in offsets {
let mut entry = TxnOffsetCommitRequestPartition::default();
entry.partition_index = tp.partition;
entry.committed_offset = *offset;
entry.committed_leader_epoch = -1;
topics.entry(tp.topic.clone()).or_default().push(entry);
}
let mut commit = TxnOffsetCommitRequest::default();
commit.transactional_id = TransactionalId(StrBytes::from_string(txn_id));
commit.group_id = GroupId(StrBytes::from_string(metadata.group_id.clone()));
commit.producer_id = ProducerId(identity.id);
commit.producer_epoch = identity.epoch;
commit.generation_id = metadata.generation_id;
commit.member_id = StrBytes::from_string(metadata.member_id.clone());
commit.group_instance_id = metadata
.group_instance_id
.clone()
.map(StrBytes::from_string);
commit.topics = topics
.into_iter()
.map(|(name, partitions)| {
let mut topic = TxnOffsetCommitRequestTopic::default();
topic.name = TopicName(StrBytes::from_string(name));
topic.partitions = partitions;
topic
})
.collect();
self.group_coordinator_call(&metadata.group_id, &commit)
.await?;
Ok(())
}
async fn group_coordinator_call(
&mut self,
group_id: &str,
req: &TxnOffsetCommitRequest,
) -> Result<()> {
let mut find = FindCoordinatorRequest::default();
find.key = StrBytes::from_string(group_id.to_owned());
find.key_type = 0;
let mut addr: Option<String> = None;
for attempt in 0..MAX_RETRIES {
let at = match addr.clone() {
Some(a) => a,
None => {
let resp: FindCoordinatorResponse = self
.cluster
.call_any(ApiKey::FindCoordinator, 3, &find)
.await?;
let code = ErrorCode(resp.error_code);
if !code.is_ok() {
if matches!(
code.disposition(),
Disposition::Retry | Disposition::FindCoordinator
) {
Self::backoff(attempt).await;
continue;
}
return Err(Error::Broker {
op: "FindCoordinator",
code: code.0,
disposition: code.disposition(),
});
}
let a = format!("{}:{}", resp.host.as_str(), resp.port);
addr = Some(a.clone());
a
}
};
let resp: TxnOffsetCommitResponse = self
.cluster
.call_at(&at, ApiKey::TxnOffsetCommit, 3, req)
.await?;
let code = ErrorCode(
resp.topics
.iter()
.flat_map(|t| t.partitions.iter())
.map(|p| p.error_code)
.find(|c| *c != 0)
.unwrap_or(0),
);
if code.is_ok() {
return Ok(());
}
match code.disposition() {
Disposition::Retry => Self::backoff(attempt).await,
Disposition::FindCoordinator => {
addr = None;
Self::backoff(attempt).await;
}
Disposition::Fatal => {
self.state.fence();
return Err(Error::Broker {
op: "TxnOffsetCommit",
code: code.0,
disposition: Disposition::Fatal,
});
}
Disposition::RefreshMetadata | Disposition::Ok => {
return Err(Error::Broker {
op: "TxnOffsetCommit",
code: code.0,
disposition: code.disposition(),
})
}
}
}
Err(Error::Broker {
op: "TxnOffsetCommit",
code: ErrorCode::COORDINATOR_NOT_AVAILABLE.0,
disposition: Disposition::Retry,
})
}
async fn enroll_all(&mut self, topic: &str, partitions: &[i32]) -> Result<()> {
let needed: Vec<i32> = partitions
.iter()
.copied()
.filter(|p| self.state.needs_enrollment(topic, *p))
.collect();
if needed.is_empty() {
return Ok(());
}
let identity = self.state.identity().ok_or(Error::Missing("producer id"))?;
let txn_id = self
.transactional_id
.clone()
.ok_or(Error::Missing("transactional id"))?;
let mut req_topic = AddPartitionsToTxnTopic::default();
req_topic.name = TopicName(StrBytes::from_string(topic.to_owned()));
req_topic.partitions.clone_from(&needed);
let mut req = AddPartitionsToTxnRequest::default();
req.v3_and_below_transactional_id = TransactionalId(StrBytes::from_string(txn_id));
req.v3_and_below_producer_id = ProducerId(identity.id);
req.v3_and_below_producer_epoch = identity.epoch;
req.v3_and_below_topics = vec![req_topic];
let _: AddPartitionsToTxnResponse = self
.coordinator_call(
"AddPartitionsToTxn",
ApiKey::AddPartitionsToTxn,
3,
&req,
|r: &AddPartitionsToTxnResponse| {
r.results_by_topic_v3_and_below
.iter()
.flat_map(|t| t.results_by_partition.iter())
.map(|p| p.partition_error_code)
.find(|c| *c != 0)
.unwrap_or(0)
},
)
.await?;
for partition in needed {
self.state.on_enrolled(topic, partition);
}
Ok(())
}
pub async fn send(
&mut self,
topic: &str,
partition: i32,
records: &[ProducerRecord],
) -> Result<i64> {
if records.is_empty() {
return Ok(-1);
}
self.enqueue(topic, partition, records).await?;
let written = self.flush().await?;
Ok(written.first().map_or(-1, |(_, _, offset)| *offset))
}
pub async fn enqueue(
&mut self,
topic: &str,
partition: i32,
records: &[ProducerRecord],
) -> Result<()> {
if records.is_empty() {
return Ok(());
}
self.enroll_all(topic, &[partition]).await?;
let batch = self.encode_for(topic, partition, records)?;
self.pending
.entry((topic.to_owned(), partition))
.or_default()
.push_back(batch);
Ok(())
}
pub fn set_metadata_max_age(&mut self, age: Duration) {
self.cluster.set_metadata_max_age(age);
}
pub fn set_linger(&mut self, linger: Duration) {
self.linger = linger;
}
pub fn set_batch_size(&mut self, bytes: usize) {
assert!(bytes > 0, "batch_size must be at least 1");
self.batch_size = bytes;
}
pub async fn produce(&mut self, topic: &str, record: ProducerRecord) -> Result<()> {
let partition = self.partition_for(topic, record.key.as_deref()).await?;
self.produce_to(topic, partition, record).await
}
pub async fn produce_to(
&mut self,
topic: &str,
partition: i32,
record: ProducerRecord,
) -> Result<()> {
let bytes = record.key.as_ref().map_or(0, bytes::Bytes::len)
+ record.value.as_ref().map_or(0, bytes::Bytes::len)
+ RECORD_OVERHEAD;
let staged = self
.staged
.entry((topic.to_owned(), partition))
.or_insert_with(|| Staged {
records: Vec::new(),
bytes: 0,
since: std::time::Instant::now(),
});
staged.records.push(record);
staged.bytes += bytes;
self.send_ready().await
}
#[must_use]
pub fn linger_deadline(&self) -> Option<std::time::Instant> {
self.staged
.values()
.map(|staged| staged.since + self.linger)
.min()
}
pub async fn tick(&mut self) -> Result<()> {
self.send_ready().await
}
async fn send_ready(&mut self) -> Result<()> {
let now = std::time::Instant::now();
let ready: Vec<(String, i32)> = self
.staged
.iter()
.filter(|(_, staged)| {
staged.bytes >= self.batch_size || now.duration_since(staged.since) >= self.linger
})
.map(|(key, _)| key.clone())
.collect();
if ready.is_empty() {
return Ok(());
}
for (topic, partition) in ready {
let Some(staged) = self.staged.remove(&(topic.clone(), partition)) else {
continue;
};
self.enqueue(&topic, partition, &staged.records).await?;
}
self.flush().await?;
Ok(())
}
async fn drain_staged(&mut self) -> Result<()> {
let keys: Vec<(String, i32)> = self.staged.keys().cloned().collect();
for (topic, partition) in keys {
let Some(staged) = self.staged.remove(&(topic.clone(), partition)) else {
continue;
};
self.enqueue(&topic, partition, &staged.records).await?;
}
Ok(())
}
pub fn set_max_in_flight(&mut self, max: usize) {
assert!(max > 0, "max_in_flight must be at least 1");
self.max_in_flight = max;
}
#[must_use]
pub fn queued(&self) -> usize {
self.pending.values().map(VecDeque::len).sum::<usize>() + self.staged.len()
}
fn encode_for(
&mut self,
topic: &str,
partition: i32,
records: &[ProducerRecord],
) -> Result<Bytes> {
let count = i32::try_from(records.len()).map_err(|_| Error::Missing("batch size"))?;
let range = self
.state
.allocate(topic, partition, count)
.map_err(Error::Producer)?;
self.encode_batch(records, range)
}
pub async fn flush(&mut self) -> Result<Vec<(String, i32, i64)>> {
self.drain_staged().await?;
let mut written: Vec<(String, i32, i64)> = Vec::new();
for attempt in 0..MAX_RETRIES {
self.pending.retain(|_, queue| !queue.is_empty());
if self.pending.is_empty() {
written.sort_unstable();
return Ok(written);
}
let mut by_broker: BTreeMap<String, Vec<(String, i32)>> = BTreeMap::new();
let mut unroutable = false;
for (topic, partition) in self.pending.keys().cloned().collect::<Vec<_>>() {
match self.cluster.leader_addr(&topic, partition).await {
Ok(addr) => by_broker.entry(addr).or_default().push((topic, partition)),
Err(Error::NoLeader { .. }) => unroutable = true,
Err(e) => return Err(e),
}
}
if by_broker.is_empty() {
if !unroutable || attempt + 1 == MAX_RETRIES {
let (topic, partition) = self.pending.keys().next().expect("non-empty").clone();
return Err(Error::NoLeader { topic, partition });
}
Self::backoff(attempt).await;
continue;
}
let depth = self.window_depth(&by_broker);
let rounds = self.send_window(&by_broker, depth).await?;
let (outcomes, transport_error) = self.collect_window(&rounds).await;
if let Some(e) = transport_error {
self.offsets.clear();
match e {
Error::Io(_) if attempt + 1 < MAX_RETRIES => {
Self::backoff(attempt).await;
continue;
}
e => return Err(e),
}
}
let mut failed_at: BTreeMap<(String, i32), usize> = BTreeMap::new();
let mut needs_refresh = false;
for (round, results) in outcomes.iter().enumerate() {
for (key, code) in results {
if code.is_ok() {
continue;
}
let earlier = failed_at.get(key).copied();
if earlier.is_some() {
continue;
}
match code.disposition() {
Disposition::RefreshMetadata => {
let (topic, partition) = key;
self.cluster.invalidate(topic, *partition);
needs_refresh = true;
}
Disposition::Retry => {}
Disposition::Fatal | Disposition::FindCoordinator | Disposition::Ok => {
self.state.fence();
return Err(Error::Broker {
op: "Produce",
code: code.0,
disposition: code.disposition(),
});
}
}
failed_at.insert(key.clone(), round);
}
}
for (round, results) in outcomes.iter().enumerate() {
for (key, _) in results {
if failed_at.get(key).is_some_and(|first| round >= *first) {
continue;
}
if let Some(queue) = self.pending.get_mut(key) {
if queue.pop_front().is_some() {
let offset = self
.offsets
.get(&(key.clone(), round))
.copied()
.unwrap_or(-1);
written.push((key.0.clone(), key.1, offset));
}
}
}
}
self.offsets.clear();
self.pending.retain(|_, queue| !queue.is_empty());
if self.pending.is_empty() {
written.sort_unstable();
return Ok(written);
}
if needs_refresh {
let mut topics: Vec<String> = self
.pending
.keys()
.map(|(topic, _)| topic.clone())
.collect();
topics.sort_unstable();
topics.dedup();
for topic in topics {
self.cluster.refresh_metadata(&topic).await?;
}
}
Self::backoff(attempt).await;
}
let (topic, partition) = self.pending.keys().next().expect("non-empty").clone();
Err(Error::NoLeader { topic, partition })
}
fn window_depth(&self, by_broker: &BTreeMap<String, Vec<(String, i32)>>) -> usize {
by_broker
.values()
.flatten()
.filter_map(|key| self.pending.get(key).map(VecDeque::len))
.max()
.unwrap_or(0)
.min(self.max_in_flight)
}
async fn send_window(
&mut self,
by_broker: &BTreeMap<String, Vec<(String, i32)>>,
depth: usize,
) -> Result<Window> {
let mut rounds: Window = Vec::with_capacity(depth);
for round in 0..depth {
let mut this_round: Vec<RoundTarget> = Vec::new();
for (addr, keys) in by_broker {
let taking: Vec<(String, i32)> = keys
.iter()
.filter(|key| self.pending.get(*key).is_some_and(|q| q.len() > round))
.cloned()
.collect();
if taking.is_empty() {
continue;
}
let req = self.produce_request(&taking, round);
match self.cluster.send_at(ApiKey::Produce, 9, addr, &req).await {
Ok(()) => this_round.push((addr.clone(), taking)),
Err(e) => {
rounds.push(this_round);
let _ = self.collect_window(&rounds).await;
return Err(e);
}
}
}
if this_round.is_empty() {
break;
}
rounds.push(this_round);
}
Ok(rounds)
}
async fn collect_window(
&mut self,
rounds: &[Vec<RoundTarget>],
) -> (Vec<Vec<((String, i32), ErrorCode)>>, Option<Error>) {
let mut out = Vec::with_capacity(rounds.len());
let mut transport_error = None;
for (round, sent) in rounds.iter().enumerate() {
let addrs: Vec<String> = sent.iter().map(|(addr, _)| addr.clone()).collect();
let responses = self
.cluster
.recv_many::<ProduceResponse>(ApiKey::Produce, &addrs)
.await;
let mut codes: Vec<((String, i32), ErrorCode)> = Vec::new();
for ((_, keys), response) in sent.iter().zip(responses) {
match response {
Ok(resp) => {
for topic_response in &resp.responses {
let topic = topic_response.name.0.to_string();
for part in &topic_response.partition_responses {
let key = (topic.clone(), part.index);
let code = ErrorCode(part.error_code);
if code.is_ok() {
self.offsets.insert((key.clone(), round), part.base_offset);
}
codes.push((key, code));
}
}
}
Err(e) => {
if transport_error.is_none() {
transport_error = Some(e);
}
for key in keys {
codes.push((key.clone(), ErrorCode::REQUEST_TIMED_OUT));
}
}
}
}
out.push(codes);
}
(out, transport_error)
}
pub async fn send_keyed(
&mut self,
topic: &str,
records: &[ProducerRecord],
) -> Result<Vec<(i32, i64)>> {
if records.is_empty() {
return Ok(Vec::new());
}
let count = self.partition_count_waiting(topic).await?;
let mut by_partition: BTreeMap<i32, Vec<ProducerRecord>> = BTreeMap::new();
for record in records {
let partition = self
.partitioner
.partition_for(record.key.as_deref(), count, &mut self.round_robin)
.ok_or_else(|| Error::NoLeader {
topic: topic.to_owned(),
partition: -1,
})?;
by_partition
.entry(partition)
.or_default()
.push(record.clone());
}
let partitions: Vec<i32> = by_partition.keys().copied().collect();
self.enroll_all(topic, &partitions).await?;
for (partition, records) in by_partition {
self.enqueue(topic, partition, &records).await?;
}
let written = self.flush().await?;
Ok(written
.into_iter()
.map(|(_, partition, offset)| (partition, offset))
.collect())
}
async fn partition_count_waiting(&mut self, topic: &str) -> Result<i32> {
for attempt in 0..MAX_RETRIES {
match self.cluster.partition_count(topic).await {
Ok(count) if count > 0 => return Ok(count),
Ok(_) | Err(Error::NoLeader { .. }) if attempt + 1 < MAX_RETRIES => {
Self::backoff(attempt).await;
}
Ok(_) => break,
Err(e) => return Err(e),
}
}
Err(Error::NoLeader {
topic: topic.to_owned(),
partition: -1,
})
}
pub async fn partition_for(&mut self, topic: &str, key: Option<&[u8]>) -> Result<i32> {
let count = self.partition_count_waiting(topic).await?;
let mut scratch = self.round_robin;
self.partitioner
.partition_for(key, count, &mut scratch)
.ok_or_else(|| Error::NoLeader {
topic: topic.to_owned(),
partition: -1,
})
}
fn encode_batch(&self, records: &[ProducerRecord], range: SequenceRange) -> Result<Bytes> {
let identity = self.state.identity().ok_or(Error::Missing("producer id"))?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX))
.unwrap_or(0);
let transactional = self.transactional_id.is_some();
let encoded: Vec<Record> = records
.iter()
.enumerate()
.map(|(i, r)| Record {
transactional,
control: false,
partition_leader_epoch: 0,
producer_id: identity.id,
producer_epoch: identity.epoch,
timestamp_type: TimestampType::Creation,
offset: i as i64,
sequence: range.base + i32::try_from(i).unwrap_or(i32::MAX),
timestamp: r.timestamp.unwrap_or(now),
key: r.key.clone(),
value: r.value.clone(),
headers: Default::default(),
})
.collect();
let mut buf = BytesMut::new();
RecordBatchEncoder::encode(
&mut buf,
encoded.iter(),
&RecordEncodeOptions {
version: 2,
compression: self.compression,
},
)
.map_err(|e| {
Error::Core(barnabas_core::Error::Codec(format!(
"encode record batch: {e}"
)))
})?;
Ok(buf.freeze())
}
fn produce_request(&self, keys: &[(String, i32)], round: usize) -> ProduceRequest {
let mut by_topic: BTreeMap<String, Vec<PartitionProduceData>> = BTreeMap::new();
for key in keys {
let Some(batch) = self.pending.get(key).and_then(|queue| queue.get(round)) else {
continue;
};
let mut data = PartitionProduceData::default();
data.index = key.1;
data.records = Some(batch.clone());
by_topic.entry(key.0.clone()).or_default().push(data);
}
let topic_data: Vec<TopicProduceData> = by_topic
.into_iter()
.map(|(name, partition_data)| {
let mut data = TopicProduceData::default();
data.name = TopicName(StrBytes::from_string(name));
data.partition_data = partition_data;
data
})
.collect();
let mut req = ProduceRequest::default();
req.acks = self.acks;
req.timeout_ms = self.timeout_ms;
req.topic_data = topic_data;
req.transactional_id = self
.transactional_id
.as_ref()
.map(|id| TransactionalId(StrBytes::from_string(id.clone())));
req
}
}
pub use kafka_protocol::records::Compression as CompressionCodec;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_record_carries_no_sequencing_of_its_own() {
let r = ProducerRecord::new(None, Some(Bytes::from_static(b"v")));
assert!(r.timestamp.is_none());
}
#[test]
fn producer_records_have_no_sequence_field() {
let r = ProducerRecord::new(Some(Bytes::from_static(b"k")), None);
assert_eq!(r.key.as_deref(), Some(&b"k"[..]));
}
}