use std::collections::{BTreeMap, BTreeSet};
use std::time::Duration;
use barnabas_core::consumer::{self, AbortedTransaction, Fetched};
use barnabas_core::records::LeanBatch;
use barnabas_core::{Disposition, ErrorCode, IsolationLevel};
use bytes::Bytes;
use kafka_protocol::messages::{
fetch_request::{FetchPartition, FetchTopic},
list_offsets_request::{ListOffsetsPartition, ListOffsetsTopic},
ApiKey, BrokerId, FetchRequest, FetchResponse, ListOffsetsRequest, ListOffsetsResponse,
TopicName,
};
use kafka_protocol::protocol::StrBytes;
use kafka_protocol::records::{Record, RecordBatchDecoder};
use crate::cluster::Cluster;
use crate::{check, Error, Result, Transport};
pub const EARLIEST: i64 = -2;
pub const LATEST: i64 = -1;
const MAX_LEADER_RETRIES: usize = 5;
const LEADER_BACKOFF: Duration = Duration::from_millis(100);
const FETCH_SESSION_ID_NOT_FOUND: i16 = 70;
const INVALID_FETCH_SESSION_EPOCH: i16 = 71;
#[derive(Debug, Default, Clone)]
struct Session {
id: i32,
epoch: i32,
known: BTreeMap<(String, i32), i64>,
}
impl Session {
fn reset(&mut self) {
self.id = 0;
self.epoch = 0;
self.known.clear();
}
}
#[derive(Debug)]
pub struct ConsumerRecords {
pub topic: String,
pub partition: i32,
pub batches: Vec<LeanBatch>,
pub fallback: Vec<Record>,
}
impl ConsumerRecords {
pub fn iter(&self) -> impl Iterator<Item = RecordRef<'_>> {
self.batches
.iter()
.flat_map(|batch| {
batch
.records
.iter()
.map(move |record| RecordRef::Lean { batch, record })
})
.chain(self.fallback.iter().map(RecordRef::Full))
}
#[must_use]
pub fn len(&self) -> usize {
self.batches.iter().map(|b| b.records.len()).sum::<usize>() + self.fallback.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub trait RebalanceListener {
fn on_revoked(&mut self, partitions: &[barnabas_core::group::TopicPartition]);
fn on_assigned(&mut self, partitions: &[barnabas_core::group::TopicPartition]);
}
#[derive(Debug, Clone, Copy)]
pub enum RecordRef<'a> {
Lean {
batch: &'a LeanBatch,
record: &'a barnabas_core::records::LeanRecord,
},
Full(&'a Record),
}
impl RecordRef<'_> {
#[must_use]
pub fn offset(&self) -> i64 {
match self {
Self::Lean { record, .. } => record.offset,
Self::Full(record) => record.offset,
}
}
#[must_use]
pub fn timestamp(&self) -> i64 {
match self {
Self::Lean { record, .. } => record.timestamp,
Self::Full(record) => record.timestamp,
}
}
#[must_use]
pub fn key(&self) -> Option<Bytes> {
match self {
Self::Lean { batch, record } => batch.key(record),
Self::Full(record) => record.key.clone(),
}
}
#[must_use]
pub fn value(&self) -> Option<Bytes> {
match self {
Self::Lean { batch, record } => batch.value(record),
Self::Full(record) => record.value.clone(),
}
}
pub fn headers(&self) -> Result<Vec<(Bytes, Option<Bytes>)>> {
match self {
Self::Lean { batch, record } => Ok(batch.headers(record)?),
Self::Full(record) => Ok(record
.headers
.iter()
.map(|(k, v)| (Bytes::copy_from_slice(k.as_str().as_bytes()), v.clone()))
.collect()),
}
}
}
pub struct Consumer<T: Transport> {
cluster: Cluster<T>,
positions: BTreeMap<(String, i32), i64>,
isolation: IsolationLevel,
max_wait: Duration,
max_bytes: i32,
max_response_bytes: i32,
sessions: BTreeMap<String, Session>,
incremental: bool,
outstanding: Option<Outstanding>,
prefetch: bool,
group: Option<crate::group::ClassicProtocol>,
reset: i64,
auto_commit: Option<Duration>,
last_auto_commit: Option<std::time::Instant>,
listener: Option<Box<dyn RebalanceListener>>,
group_timeouts: Option<(Duration, Duration)>,
paused: BTreeSet<(String, i32)>,
expansions: BTreeMap<String, (i32, i32)>,
generation: u64,
}
type Planned = (String, Vec<(String, i32)>, FetchRequest);
struct Outstanding {
groups: Vec<(String, Vec<(String, i32)>)>,
generation: u64,
}
impl<T: Transport> Consumer<T> {
pub fn builder(transport: T) -> crate::builder::ConsumerBuilder<T> {
crate::builder::ConsumerBuilder::new(transport)
}
pub(crate) fn from_cluster(cluster: Cluster<T>, isolation: IsolationLevel) -> Self {
Self {
cluster,
positions: BTreeMap::new(),
paused: BTreeSet::new(),
expansions: BTreeMap::new(),
isolation,
max_wait: Duration::from_millis(500),
max_bytes: 10 * 1024 * 1024,
max_response_bytes: 64 * 1024 * 1024,
sessions: BTreeMap::new(),
incremental: true,
outstanding: None,
prefetch: true,
group: None,
reset: EARLIEST,
auto_commit: None,
last_auto_commit: None,
listener: None,
group_timeouts: None,
generation: 0,
}
}
pub async fn new(
transport: T,
bootstrap: &[String],
client_id: &str,
isolation: IsolationLevel,
) -> Result<Self> {
Ok(Self {
cluster: Cluster::connect(transport, bootstrap, client_id).await?,
positions: BTreeMap::new(),
paused: BTreeSet::new(),
expansions: BTreeMap::new(),
isolation,
max_wait: Duration::from_millis(500),
max_bytes: 10 * 1024 * 1024,
max_response_bytes: 64 * 1024 * 1024,
sessions: BTreeMap::new(),
incremental: true,
outstanding: None,
prefetch: true,
group: None,
reset: EARLIEST,
auto_commit: None,
last_auto_commit: None,
listener: None,
group_timeouts: None,
generation: 0,
})
}
pub async fn for_partition(
transport: T,
bootstrap: &[String],
client_id: &str,
topic: &str,
partition: i32,
offset: i64,
isolation: IsolationLevel,
) -> Result<Self> {
let mut me = Self::new(transport, bootstrap, client_id, isolation).await?;
me.assign(topic, partition, offset).await?;
Ok(me)
}
pub async fn assign(&mut self, topic: &str, partition: i32, offset: i64) -> Result<()> {
self.discard_outstanding().await;
self.cluster.refresh_metadata(topic).await?;
self.positions.insert((topic.to_owned(), partition), offset);
if offset == EARLIEST || offset == LATEST {
let resolved = self.list_offset(topic, partition, offset).await?;
self.positions
.insert((topic.to_owned(), partition), resolved);
}
for session in self.sessions.values_mut() {
session.reset();
}
self.generation += 1;
Ok(())
}
pub async fn partition_count(&mut self, topic: &str) -> Result<i32> {
self.cluster.partition_count(topic).await
}
pub async fn subscribe(
&mut self,
group_id: &str,
topics: Vec<String>,
assignor: Box<dyn barnabas_core::group::Assignor>,
reset: i64,
) -> Result<()> {
self.discard_outstanding().await;
self.positions.clear();
self.paused.clear();
self.reset = reset;
let mut protocol =
crate::group::ClassicProtocol::new(group_id.to_owned(), topics, assignor);
if let Some((session, rebalance)) = self.group_timeouts {
protocol.set_session_timeout(i32::try_from(session.as_millis()).unwrap_or(i32::MAX));
protocol
.set_rebalance_timeout(i32::try_from(rebalance.as_millis()).unwrap_or(i32::MAX));
}
self.group = Some(protocol);
self.generation += 1;
Ok(())
}
pub fn set_auto_commit(&mut self, interval: Option<Duration>) {
self.auto_commit = interval;
self.last_auto_commit = None;
}
pub fn set_rebalance_listener(&mut self, listener: Box<dyn RebalanceListener>) {
self.listener = Some(listener);
}
pub fn set_group_timeouts(&mut self, session: Duration, rebalance: Duration) {
self.group_timeouts = Some((session, rebalance));
}
pub async fn heartbeat(&mut self) -> Result<bool> {
if self.group.is_none() {
return Ok(false);
}
self.advance_group().await
}
pub async fn unsubscribe(&mut self) -> Result<()> {
self.discard_outstanding().await;
self.positions.clear();
self.generation += 1;
let Some(mut group) = self.group.take() else {
return Ok(());
};
crate::group::GroupProtocol::leave(&mut group, &mut self.cluster).await
}
pub async fn commit(&mut self) -> Result<()> {
self.discard_outstanding().await;
let offsets: BTreeMap<barnabas_core::group::TopicPartition, i64> = self
.positions
.iter()
.map(|((topic, partition), offset)| {
(
barnabas_core::group::TopicPartition::new(topic.clone(), *partition),
*offset,
)
})
.collect();
let Some(group) = self.group.as_mut() else {
return Err(Error::Missing("a group to commit to"));
};
crate::group::GroupProtocol::commit(group, &mut self.cluster, &offsets).await
}
pub fn set_metadata_max_age(&mut self, age: Duration) {
self.cluster.set_metadata_max_age(age);
}
pub fn take_expansions(&mut self) -> Vec<(String, i32, i32)> {
std::mem::take(&mut self.expansions)
.into_iter()
.map(|(topic, (before, after))| (topic, before, after))
.collect()
}
async fn check_for_expansion(&mut self) -> Result<()> {
let mut topics: Vec<String> = match self.group.as_ref() {
Some(group) => crate::group::GroupProtocol::<T>::topics(group),
None => self
.positions
.keys()
.map(|(topic, _)| topic.clone())
.collect(),
};
topics.sort();
topics.dedup();
topics.retain(|topic| self.cluster.is_metadata_stale(topic));
if topics.is_empty() {
return Ok(());
}
self.discard_outstanding().await;
let mut grew = false;
for topic in topics {
if let Ok(Some((before, after))) = self.cluster.refresh_if_stale(&topic).await {
if self.group.is_some() {
grew = true;
} else {
self.expansions.insert(topic, (before, after));
}
}
}
if grew {
if let Some(group) = self.group.as_mut() {
crate::group::GroupProtocol::<T>::request_rejoin(group);
}
}
Ok(())
}
#[must_use]
pub fn positions(&self) -> BTreeMap<barnabas_core::group::TopicPartition, i64> {
self.positions
.iter()
.map(|((topic, partition), offset)| {
(
barnabas_core::group::TopicPartition::new(topic.clone(), *partition),
*offset,
)
})
.collect()
}
#[must_use]
pub fn group_metadata(&self) -> Option<crate::group::GroupMetadata> {
self.group
.as_ref()
.and_then(crate::group::GroupProtocol::<T>::group_metadata)
}
async fn maybe_auto_commit(&mut self) -> Result<()> {
let Some(interval) = self.auto_commit else {
return Ok(());
};
let due = self
.last_auto_commit
.is_none_or(|last| last.elapsed() >= interval);
if !due || self.positions.is_empty() {
return Ok(());
}
match self.commit().await {
Ok(())
| Err(Error::Broker {
op: "OffsetCommit", ..
}) => {}
Err(e) => return Err(e),
}
self.last_auto_commit = Some(std::time::Instant::now());
Ok(())
}
async fn advance_group(&mut self) -> Result<bool> {
const MAX_STEPS: usize = 20;
let mut changed = false;
for _ in 0..MAX_STEPS {
let Some(mut group) = self.group.take() else {
return Ok(changed);
};
let outcome = crate::group::GroupProtocol::advance(&mut group, &mut self.cluster).await;
let settled = match &outcome {
Ok(crate::group::Membership::Assigned(partitions)) => {
let wanted: BTreeMap<(String, i32), ()> = partitions
.iter()
.map(|tp| ((tp.topic.clone(), tp.partition), ()))
.collect();
let same = wanted.len() == self.positions.len()
&& wanted.keys().all(|k| self.positions.contains_key(k));
if !same {
let committed = crate::group::GroupProtocol::committed(
&mut group,
&mut self.cluster,
partitions,
)
.await?;
self.positions.clear();
for tp in partitions {
let start = committed.get(tp).copied().unwrap_or(self.reset);
self.positions
.insert((tp.topic.clone(), tp.partition), start);
}
if let Some(listener) = self.listener.as_mut() {
listener.on_assigned(partitions);
}
changed = true;
}
true
}
Ok(crate::group::Membership::Revoked(lost)) => {
if let Some(listener) = self.listener.as_mut() {
listener.on_revoked(lost);
}
for tp in lost {
self.positions.remove(&(tp.topic.clone(), tp.partition));
self.paused.remove(&(tp.topic.clone(), tp.partition));
}
changed |= !lost.is_empty();
false
}
Ok(crate::group::Membership::InProgress) | Err(_) => false,
};
self.group = Some(group);
outcome?;
if settled {
break;
}
}
if changed {
self.generation += 1;
for session in self.sessions.values_mut() {
session.reset();
}
let unresolved: Vec<(String, i32, i64)> = self
.positions
.iter()
.filter(|(_, offset)| **offset == EARLIEST || **offset == LATEST)
.map(|((topic, partition), offset)| (topic.clone(), *partition, *offset))
.collect();
for (topic, partition, offset) in unresolved {
let resolved = self.list_offset(&topic, partition, offset).await?;
self.positions.insert((topic, partition), resolved);
}
}
Ok(changed)
}
pub fn remove(&mut self, topic: &str, partition: i32) {
self.positions.remove(&(topic.to_owned(), partition));
self.paused.remove(&(topic.to_owned(), partition));
self.generation += 1;
for session in self.sessions.values_mut() {
session.reset();
}
}
pub fn pause(&mut self, partitions: &[barnabas_core::group::TopicPartition]) {
for tp in partitions {
self.paused.insert((tp.topic.clone(), tp.partition));
}
self.on_fetch_set_changed();
}
pub fn resume(&mut self, partitions: &[barnabas_core::group::TopicPartition]) {
for tp in partitions {
self.paused.remove(&(tp.topic.clone(), tp.partition));
}
self.on_fetch_set_changed();
}
pub fn paused(&self) -> impl Iterator<Item = (&str, i32)> {
self.paused
.iter()
.map(|(topic, partition)| (topic.as_str(), *partition))
}
#[must_use]
pub fn is_paused(&self, topic: &str, partition: i32) -> bool {
self.paused.contains(&(topic.to_owned(), partition))
}
fn on_fetch_set_changed(&mut self) {
self.generation += 1;
for session in self.sessions.values_mut() {
session.reset();
}
}
fn fetchable(&self) -> Vec<(String, i32)> {
self.positions
.keys()
.filter(|key| !self.paused.contains(*key))
.cloned()
.collect()
}
pub fn assignments(&self) -> impl Iterator<Item = (&str, i32)> {
self.positions
.keys()
.map(|(topic, partition)| (topic.as_str(), *partition))
}
#[must_use]
pub fn position_of(&self, topic: &str, partition: i32) -> Option<i64> {
self.positions.get(&(topic.to_owned(), partition)).copied()
}
#[must_use]
pub fn position(&self) -> i64 {
assert_eq!(
self.positions.len(),
1,
"position() needs exactly one assignment; use position_of()"
);
*self.positions.values().next().expect("checked length")
}
pub fn seek_to(&mut self, topic: &str, partition: i32, offset: i64) {
self.positions.insert((topic.to_owned(), partition), offset);
self.generation += 1;
}
pub fn seek(&mut self, offset: i64) {
assert_eq!(
self.positions.len(),
1,
"seek() needs exactly one assignment; use seek_to()"
);
let key = self
.positions
.keys()
.next()
.expect("checked length")
.clone();
self.positions.insert(key, offset);
self.generation += 1;
}
pub fn set_incremental_fetch(&mut self, incremental: bool) {
self.incremental = incremental;
if !incremental {
self.sessions.clear();
}
self.generation += 1;
}
pub fn set_prefetch(&mut self, prefetch: bool) {
self.prefetch = prefetch;
self.generation += 1;
}
pub fn set_max_wait(&mut self, max_wait: Duration) {
self.max_wait = max_wait;
self.generation += 1;
}
#[must_use]
pub fn connection_count(&self) -> usize {
self.cluster.connection_count()
}
#[must_use]
pub fn metadata_leader(&self, topic: &str, partition: i32) -> Option<String> {
self.cluster
.metadata()
.leader_for(topic, partition)
.map(barnabas_core::BrokerAddr::addr)
}
pub async fn list_offset(
&mut self,
topic: &str,
partition: i32,
timestamp: i64,
) -> Result<i64> {
self.discard_outstanding().await;
let mut req_partition = ListOffsetsPartition::default();
req_partition.partition_index = partition;
req_partition.timestamp = timestamp;
let mut req_topic = ListOffsetsTopic::default();
req_topic.name = TopicName(StrBytes::from_string(topic.to_owned()));
req_topic.partitions = vec![req_partition];
let mut req = ListOffsetsRequest::default();
req.replica_id = BrokerId(-1);
req.isolation_level = self.isolation.as_i8();
req.topics = vec![req_topic];
for attempt in 0..=MAX_LEADER_RETRIES {
let addr = match self.cluster.leader_addr(topic, partition).await {
Ok(addr) => addr,
Err(e @ Error::NoLeader { .. }) => {
if attempt == MAX_LEADER_RETRIES {
return Err(e);
}
T::sleep(LEADER_BACKOFF).await;
continue;
}
Err(e) => return Err(e),
};
let resp: ListOffsetsResponse = self
.cluster
.call_at(&addr, ApiKey::ListOffsets, 7, &req)
.await?;
let found = resp
.topics
.iter()
.flat_map(|t| t.partitions.iter())
.find(|p| p.partition_index == partition)
.ok_or(Error::Missing("partition"))?;
let code = ErrorCode(found.error_code);
if code.disposition() == Disposition::RefreshMetadata {
self.cluster.invalidate(topic, partition);
if attempt == MAX_LEADER_RETRIES {
return Err(Error::Broker {
op: "ListOffsets",
code: code.0,
disposition: code.disposition(),
});
}
self.cluster.refresh_metadata(topic).await?;
T::sleep(LEADER_BACKOFF).await;
continue;
}
check("ListOffsets", found.error_code)?;
return Ok(found.offset);
}
unreachable!("the loop returns on its last attempt")
}
async fn list_offsets_many(
&mut self,
want: &[(barnabas_core::group::TopicPartition, i64)],
) -> Result<BTreeMap<barnabas_core::group::TopicPartition, (i64, i64)>> {
self.discard_outstanding().await;
let mut found = BTreeMap::new();
if want.is_empty() {
return Ok(found);
}
let mut remaining: Vec<(barnabas_core::group::TopicPartition, i64)> = want.to_vec();
for attempt in 0..=MAX_LEADER_RETRIES {
let mut by_leader: BTreeMap<String, Vec<(barnabas_core::group::TopicPartition, i64)>> =
BTreeMap::new();
let mut no_leader: Option<Error> = None;
for (tp, timestamp) in &remaining {
match self.cluster.leader_addr(&tp.topic, tp.partition).await {
Ok(addr) => by_leader
.entry(addr)
.or_default()
.push((tp.clone(), *timestamp)),
Err(e @ Error::NoLeader { .. }) => no_leader = Some(e),
Err(e) => return Err(e),
}
}
let mut retry: Vec<(barnabas_core::group::TopicPartition, i64)> = Vec::new();
let mut refresh: Vec<String> = Vec::new();
for (addr, group) in by_leader {
let mut topics: BTreeMap<String, Vec<ListOffsetsPartition>> = BTreeMap::new();
for (tp, timestamp) in &group {
let mut entry = ListOffsetsPartition::default();
entry.partition_index = tp.partition;
entry.timestamp = *timestamp;
topics.entry(tp.topic.clone()).or_default().push(entry);
}
let mut req = ListOffsetsRequest::default();
req.replica_id = BrokerId(-1);
req.isolation_level = self.isolation.as_i8();
req.topics = topics
.into_iter()
.map(|(name, partitions)| {
let mut topic = ListOffsetsTopic::default();
topic.name = TopicName(StrBytes::from_string(name));
topic.partitions = partitions;
topic
})
.collect();
let resp: ListOffsetsResponse = self
.cluster
.call_at(&addr, ApiKey::ListOffsets, 7, &req)
.await?;
for topic in &resp.topics {
for partition in &topic.partitions {
let tp = barnabas_core::group::TopicPartition::new(
topic.name.0.to_string(),
partition.partition_index,
);
let code = ErrorCode(partition.error_code);
if code.is_ok() {
if partition.offset >= 0 {
found.insert(tp, (partition.offset, partition.timestamp));
}
continue;
}
if code.disposition() == Disposition::RefreshMetadata {
self.cluster.invalidate(&tp.topic, tp.partition);
refresh.push(tp.topic.clone());
let timestamp = group
.iter()
.find(|(w, _)| *w == tp)
.map_or(LATEST, |(_, t)| *t);
retry.push((tp, timestamp));
continue;
}
check("ListOffsets", partition.error_code)?;
}
}
}
if let Some(e) = no_leader {
if retry.is_empty() && attempt == MAX_LEADER_RETRIES {
return Err(e);
}
for (tp, timestamp) in &remaining {
if !found.contains_key(tp) && !retry.iter().any(|(r, _)| r == tp) {
retry.push((tp.clone(), *timestamp));
}
}
}
if retry.is_empty() {
return Ok(found);
}
if attempt == MAX_LEADER_RETRIES {
return Err(Error::Broker {
op: "ListOffsets",
code: ErrorCode::NOT_LEADER_OR_FOLLOWER.0,
disposition: Disposition::RefreshMetadata,
});
}
refresh.sort();
refresh.dedup();
for topic in refresh {
self.cluster.refresh_metadata(&topic).await?;
}
T::sleep(LEADER_BACKOFF).await;
remaining = retry;
}
unreachable!("the loop returns on its last attempt")
}
pub async fn end_offsets(
&mut self,
partitions: &[barnabas_core::group::TopicPartition],
) -> Result<BTreeMap<barnabas_core::group::TopicPartition, i64>> {
let want: Vec<_> = partitions.iter().cloned().map(|tp| (tp, LATEST)).collect();
Ok(self
.list_offsets_many(&want)
.await?
.into_iter()
.map(|(tp, (offset, _))| (tp, offset))
.collect())
}
pub async fn beginning_offsets(
&mut self,
partitions: &[barnabas_core::group::TopicPartition],
) -> Result<BTreeMap<barnabas_core::group::TopicPartition, i64>> {
let want: Vec<_> = partitions
.iter()
.cloned()
.map(|tp| (tp, EARLIEST))
.collect();
Ok(self
.list_offsets_many(&want)
.await?
.into_iter()
.map(|(tp, (offset, _))| (tp, offset))
.collect())
}
pub async fn offsets_for_times(
&mut self,
want: &[(barnabas_core::group::TopicPartition, i64)],
) -> Result<BTreeMap<barnabas_core::group::TopicPartition, (i64, i64)>> {
self.list_offsets_many(want).await
}
pub async fn lag(&mut self) -> Result<BTreeMap<barnabas_core::group::TopicPartition, i64>> {
let positions = self.positions();
let assigned: Vec<_> = positions.keys().cloned().collect();
let ends = self.end_offsets(&assigned).await?;
Ok(positions
.into_iter()
.filter_map(|(tp, position)| ends.get(&tp).map(|end| (tp, (end - position).max(0))))
.collect())
}
pub async fn committed(
&mut self,
partitions: &[barnabas_core::group::TopicPartition],
) -> Result<BTreeMap<barnabas_core::group::TopicPartition, i64>> {
self.discard_outstanding().await;
let Some(group) = self.group.as_mut() else {
return Err(Error::Missing("a group to read commits from"));
};
crate::group::GroupProtocol::committed(group, &mut self.cluster, partitions).await
}
async fn issue_fetch(&mut self) -> Result<()> {
let mut by_broker: BTreeMap<String, Vec<(String, i32)>> = BTreeMap::new();
for (topic, partition) in self.fetchable() {
let addr = self.cluster.leader_addr(&topic, partition).await?;
by_broker.entry(addr).or_default().push((topic, partition));
}
let planned: Vec<Planned> = by_broker
.into_iter()
.map(|(addr, partitions)| {
let req = self.fetch_request(&addr, &partitions);
(addr, partitions, req)
})
.collect();
let mut sent: Vec<(String, Vec<(String, i32)>)> = Vec::with_capacity(planned.len());
let mut failure = None;
for (addr, partitions, req) in planned {
match self.cluster.send_at(ApiKey::Fetch, 12, &addr, &req).await {
Ok(()) => sent.push((addr, partitions)),
Err(e) => {
failure = Some(e);
break;
}
}
}
if sent.is_empty() {
return failure.map_or(Ok(()), Err);
}
self.outstanding = Some(Outstanding {
groups: sent,
generation: self.generation,
});
failure.map_or(Ok(()), Err)
}
async fn start_prefetch(&mut self) {
if self.prefetch && !self.fetchable().is_empty() {
let _ = self.issue_fetch().await;
}
}
async fn discard_outstanding(&mut self) {
let Some(outstanding) = self.outstanding.take() else {
return;
};
let addrs: Vec<String> = outstanding
.groups
.iter()
.map(|(addr, _)| addr.clone())
.collect();
self.cluster
.discard_many::<FetchResponse>(ApiKey::Fetch, &addrs)
.await;
for session in self.sessions.values_mut() {
session.reset();
}
}
fn fetch_request(&self, addr: &str, partitions: &[(String, i32)]) -> FetchRequest {
let session = self.sessions.get(addr);
let incremental = self.incremental && session.is_some_and(|s| s.id != 0);
let mut by_topic: BTreeMap<&str, Vec<i32>> = BTreeMap::new();
for (topic, partition) in partitions {
if incremental {
let known = session
.and_then(|s| s.known.get(&(topic.clone(), *partition)))
.copied();
let current = self.positions.get(&(topic.clone(), *partition)).copied();
if known == current {
continue;
}
}
by_topic.entry(topic.as_str()).or_default().push(*partition);
}
let topics: Vec<FetchTopic> = by_topic
.into_iter()
.map(|(topic, partitions)| {
let mut fetch_topic = FetchTopic::default();
fetch_topic.topic = TopicName(StrBytes::from_string(topic.to_owned()));
fetch_topic.partitions = partitions
.into_iter()
.map(|partition| {
let mut fetch_partition = FetchPartition::default();
fetch_partition.partition = partition;
fetch_partition.fetch_offset = self
.positions
.get(&(topic.to_owned(), partition))
.copied()
.unwrap_or(0);
fetch_partition.partition_max_bytes = self.max_bytes;
fetch_partition.current_leader_epoch = -1;
fetch_partition.log_start_offset = -1;
fetch_partition
})
.collect();
fetch_topic
})
.collect();
let mut req = FetchRequest::default();
req.replica_id = BrokerId(-1);
req.max_wait_ms = i32::try_from(self.max_wait.as_millis()).unwrap_or(i32::MAX);
req.min_bytes = 1;
req.max_bytes = self.max_response_bytes;
req.isolation_level = self.isolation.as_i8();
req.topics = topics;
if self.incremental {
req.session_id = session.map_or(0, |s| s.id);
req.session_epoch = session.map_or(0, |s| s.epoch);
} else {
req.session_epoch = -1;
}
req
}
}
fn decode_records(mut bytes: Bytes) -> Result<Vec<Record>> {
const HEADER: usize = 12;
let mut all = Vec::new();
while bytes.len() >= HEADER {
let batch_length = i32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
let Ok(batch_length) = usize::try_from(batch_length) else {
return Err(Error::Core(barnabas_core::Error::Codec(format!(
"record batch declares a negative length: {batch_length}"
))));
};
if bytes.len() < HEADER + batch_length {
break;
}
let set = RecordBatchDecoder::decode(&mut bytes).map_err(|e| {
Error::Core(barnabas_core::Error::Codec(format!(
"decode record batch: {e}"
)))
})?;
all.extend(set.records);
}
Ok(all)
}
impl<T: Transport> Consumer<T> {
pub async fn poll(&mut self) -> Result<Vec<ConsumerRecords>> {
self.check_for_expansion().await?;
if self.group.is_some() {
self.maybe_auto_commit().await?;
let changed = self.advance_group().await?;
if changed {
self.discard_outstanding().await;
}
}
if self.fetchable().is_empty() {
return Ok(Vec::new());
}
if self
.outstanding
.as_ref()
.is_some_and(|o| o.generation != self.generation)
{
self.discard_outstanding().await;
}
if self.outstanding.is_none() {
self.issue_fetch().await?;
}
let groups = self.outstanding.take().expect("just issued").groups;
let addrs: Vec<String> = groups.iter().map(|(addr, _)| addr.clone()).collect();
let responses = self
.cluster
.recv_many::<FetchResponse>(ApiKey::Fetch, &addrs)
.await;
let mut out = Vec::new();
for ((addr, partitions), response) in groups.into_iter().zip(responses) {
let resp = response?;
if matches!(
resp.error_code,
FETCH_SESSION_ID_NOT_FOUND | INVALID_FETCH_SESSION_EPOCH
) {
self.sessions.entry(addr.clone()).or_default().reset();
continue;
}
check("Fetch", resp.error_code)?;
if self.incremental {
let session = self.sessions.entry(addr.clone()).or_default();
session.id = resp.session_id;
session.epoch = session.epoch.wrapping_add(1).max(1);
for (topic, partition) in &partitions {
if let Some(offset) = self.positions.get(&(topic.clone(), *partition)) {
session.known.insert((topic.clone(), *partition), *offset);
}
}
}
for topic_response in &resp.responses {
let topic = topic_response.topic.0.to_string();
for part in &topic_response.partitions {
check("Fetch partition", part.error_code)?;
let key = (topic.clone(), part.partition_index);
let Some(fetch_offset) = self.positions.get(&key).copied() else {
continue;
};
let Some(bytes) = part.records.clone().filter(|b| !b.is_empty()) else {
continue;
};
let Some(decoded) = barnabas_core::records::decode_lean(&bytes)? else {
let records = decode_records(bytes)?;
let aborted = aborted_of(part);
let Fetched {
records,
next_offset,
} = consumer::filter(
records,
&aborted,
part.last_stable_offset,
self.isolation,
fetch_offset,
);
self.positions.insert(key, next_offset);
if !records.is_empty() {
out.push(ConsumerRecords {
topic: topic.clone(),
partition: part.partition_index,
batches: Vec::new(),
fallback: records,
});
}
continue;
};
let aborted = aborted_of(part);
let (batches, next_offset) = barnabas_core::records::filter_batches(
decoded,
&aborted,
part.last_stable_offset,
self.isolation,
fetch_offset,
);
self.positions.insert(key, next_offset);
if !batches.is_empty() {
out.push(ConsumerRecords {
topic: topic.clone(),
partition: part.partition_index,
batches,
fallback: Vec::new(),
});
}
}
}
}
self.start_prefetch().await;
Ok(out)
}
}
fn aborted_of(
part: &kafka_protocol::messages::fetch_response::PartitionData,
) -> Vec<AbortedTransaction> {
part.aborted_transactions
.as_ref()
.map(|list| {
list.iter()
.map(|a| AbortedTransaction {
producer_id: a.producer_id.0,
first_offset: a.first_offset,
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::BytesMut;
use kafka_protocol::records::{
Compression, Record, RecordBatchEncoder, RecordEncodeOptions, TimestampType,
};
fn batch(base_offset: i64, count: usize) -> Bytes {
let records: Vec<Record> = (0..count)
.map(|i| Record {
transactional: false,
control: false,
partition_leader_epoch: 0,
producer_id: -1,
producer_epoch: -1,
timestamp_type: TimestampType::Creation,
offset: base_offset + i as i64,
sequence: i as i32,
timestamp: 0,
key: None,
value: Some(Bytes::from(format!("v{i}"))),
headers: Default::default(),
})
.collect();
let mut buf = BytesMut::new();
RecordBatchEncoder::encode(
&mut buf,
records.iter(),
&RecordEncodeOptions {
version: 2,
compression: Compression::None,
},
)
.expect("encode");
buf.freeze()
}
#[test]
fn whole_batches_decode() {
let mut wire = BytesMut::new();
wire.extend_from_slice(&batch(0, 3));
wire.extend_from_slice(&batch(3, 2));
let records = decode_records(wire.freeze()).expect("decode");
assert_eq!(records.len(), 5);
}
#[test]
fn a_truncated_trailing_batch_is_ignored() {
let complete = batch(0, 3);
let partial = batch(3, 2);
let mut wire = BytesMut::new();
wire.extend_from_slice(&complete);
wire.extend_from_slice(&partial[..partial.len() - 4]);
let records = decode_records(wire.freeze()).expect("a truncated tail is not an error");
assert_eq!(
records.len(),
3,
"the complete batch must survive and the fragment must be dropped"
);
}
#[test]
fn a_fragment_shorter_than_a_header_is_ignored() {
let mut wire = BytesMut::new();
wire.extend_from_slice(&batch(0, 1));
wire.extend_from_slice(&[0u8; 5]);
assert_eq!(decode_records(wire.freeze()).expect("decode").len(), 1);
}
#[test]
fn a_negative_batch_length_is_an_error() {
let mut wire = BytesMut::new();
wire.extend_from_slice(&0i64.to_be_bytes());
wire.extend_from_slice(&(-1i32).to_be_bytes());
wire.extend_from_slice(&[0u8; 32]);
assert!(decode_records(wire.freeze()).is_err());
}
}