mod acks;
mod builder;
mod commit;
mod completed_fetch;
mod config;
mod membership;
mod request_manager;
mod session;
mod state;
mod stream;
#[cfg(test)]
mod tests;
#[cfg(all(test, feature = "test-broker"))]
mod broker_tests;
pub use builder::ShareConsumerBuilder;
pub use commit::{AcknowledgementCommit, AcknowledgementCommitCallback, CommitResults};
pub use config::{AcknowledgementMode, AcquireMode};
pub use stream::ShareConsumerStream;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use tracing::{Instrument, debug, info, warn};
use crate::PartitionId;
use crate::client::CloseOptions;
use crate::consumer::ConsumerRecord;
use crate::error::{KrafkaError, Result};
use acks::AckType;
use state::Inner;
const DEFAULT_CLOSE_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub struct ShareConsumer(Arc<Inner>);
impl std::fmt::Debug for ShareConsumer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShareConsumer")
.field("group_id", &self.0.config.group_id)
.field("closed", &self.0.closed.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
struct Demand<'a>(&'a Inner);
impl Drop for Demand<'_> {
fn drop(&mut self) {
self.0.state.lock().fetch_wanted = false;
}
}
impl ShareConsumer {
pub async fn subscribe(&self, topics: impl IntoIterator<Item = impl AsRef<str>>) -> Result<()> {
if self.is_closed() {
return Err(KrafkaError::closed("share consumer is closed"));
}
let mut subscription: Vec<String> =
topics.into_iter().map(|t| t.as_ref().to_string()).collect();
subscription.sort_unstable();
subscription.dedup();
let topics: Vec<&str> = subscription.iter().map(String::as_str).collect();
crate::protocol::validate_topic_names(topics.iter().copied())?;
{
let mut member = self.0.member.lock();
member.subscription = subscription.clone();
member.subscription_acknowledged = false;
}
self.0.metadata.refresh_for_topics(Some(&topics)).await?;
membership::join(&self.0).await?;
membership::start(&self.0);
debug!(group = %self.0.config.group_id, topics = topics.len(), "subscribed");
Ok(())
}
pub fn subscription(&self) -> std::collections::HashSet<String> {
self.0.member.lock().subscription.iter().cloned().collect()
}
pub fn assignment(&self) -> std::collections::HashMap<String, Vec<PartitionId>> {
let state = self.0.state.lock();
let mut out: std::collections::HashMap<String, Vec<PartitionId>> =
std::collections::HashMap::new();
for assigned in &state.assigned {
out.entry(assigned.partition.topic.clone())
.or_default()
.push(assigned.partition.partition);
}
out
}
pub fn member_id(&self) -> String {
self.0.member.lock().member_id.clone()
}
pub fn member_epoch(&self) -> i32 {
self.0.member.lock().member_epoch
}
pub async fn poll(&self, timeout: Duration) -> Result<Vec<ConsumerRecord>> {
let span = self.span(crate::tracing_ext::poll_span, "poll");
let result = self.poll_records(timeout).instrument(span.clone()).await;
crate::tracing_ext::record_poll_outcome(&span, result.as_ref().map(Vec::len));
result
}
fn span(
&self,
make: fn(Option<&str>, &str) -> tracing::Span,
operation: &'static str,
) -> tracing::Span {
let span = make(
Some(self.0.config.group_id.as_str()),
self.0.metrics_source.client_id(),
);
if !span.is_disabled() {
let member = self.0.member.lock();
crate::tracing_ext::record_destination(
&span,
operation,
member.subscription.iter().map(String::as_str),
);
}
span
}
async fn poll_records(&self, timeout: Duration) -> Result<Vec<ConsumerRecord>> {
let _timer = self.0.metrics.poll_latency.start();
self.0.metrics.polls.inc();
let deadline = tokio::time::Instant::now() + timeout;
let max = self.0.config.max_poll_records.max(1) as usize;
let result = self.next_records(Some(deadline), max).await;
match result {
Ok(Some(records)) => Ok(records),
Ok(None) => {
self.0.metrics.empty_polls.inc();
Ok(Vec::new())
}
Err(error) => {
self.0.metrics.record_error();
Err(error)
}
}
}
pub async fn recv(&self) -> Result<Option<ConsumerRecord>> {
let span = self.span(crate::tracing_ext::poll_span, "poll");
let result = match self.next_records(None, 1).instrument(span.clone()).await {
Ok(records) => Ok(records.and_then(|mut r| r.pop())),
Err(KrafkaError::Closed { .. }) => Ok(None),
Err(error) => Err(error),
};
crate::tracing_ext::record_poll_outcome(
&span,
result.as_ref().map(|record| usize::from(record.is_some())),
);
result
}
async fn next_records(
&self,
deadline: Option<tokio::time::Instant>,
max: usize,
) -> Result<Option<Vec<ConsumerRecord>>> {
let inner = &self.0;
let _serial = inner.poll_lock.lock().await;
self.check_usable()?;
if inner.config.acknowledgement_mode == AcknowledgementMode::Explicit
&& !inner.state.lock().outstanding.is_empty()
{
return Err(KrafkaError::illegal_state(
"every record the previous poll returned must be acknowledged first",
));
}
if inner.accept_delivered() {
inner.wake_nodes();
}
if let Some(error) = inner.state.lock().deferred_error.take() {
return Err(error);
}
let mut demand = None;
loop {
let ready = inner.records_ready.notified();
tokio::pin!(ready);
ready.as_mut().enable();
if let Some(records) = inner.take_records(max)? {
return Ok(Some(records));
}
self.check_usable()?;
if demand.is_none() {
inner.state.lock().fetch_wanted = true;
demand = Some(Demand(inner));
inner.wake_nodes();
}
match deadline {
Some(deadline) => {
if tokio::time::timeout_at(deadline, ready).await.is_err() {
return Ok(None);
}
}
None => ready.await,
}
}
}
fn check_usable(&self) -> Result<()> {
if self.is_closed() {
return Err(KrafkaError::closed("share consumer is closed"));
}
if self.0.wakeup.swap(false, Ordering::AcqRel) {
return Err(KrafkaError::Wakeup);
}
if let Some(error) = &self.0.state.lock().fatal {
return Err(error.clone());
}
Ok(())
}
pub fn ack(&self, record: &ConsumerRecord) -> Result<()> {
self.0.acknowledge(record, AckType::Accept)
}
pub fn release(&self, record: &ConsumerRecord) -> Result<()> {
self.0.acknowledge(record, AckType::Release)
}
pub fn reject(&self, record: &ConsumerRecord) -> Result<()> {
self.0.acknowledge(record, AckType::Reject)
}
pub fn renew(&self, record: &ConsumerRecord) -> Result<()> {
self.0.acknowledge(record, AckType::Renew)
}
pub async fn commit(&self) -> Result<CommitResults> {
let deadline = tokio::time::Instant::now() + self.0.config.request_timeout;
let span = self.span(crate::tracing_ext::commit_span, "commit");
let result = commit::commit(&self.0, deadline)
.instrument(span.clone())
.await;
match &result {
Err(error) => crate::tracing_ext::record_error(&span, error),
Ok(results) => {
if let Some(error) = results.values().find_map(|r| r.as_ref().err()) {
crate::tracing_ext::record_error(&span, error);
}
}
}
result
}
pub fn stream(&self) -> ShareConsumerStream<'_> {
ShareConsumerStream::new(self)
}
pub async fn unsubscribe(&self) {
membership::stop(&self.0);
if let Err(error) = self.commit().await {
warn!("committing acknowledgements during unsubscribe failed: {error}");
}
if let Err(error) = membership::leave(&self.0).await {
warn!("leaving the share group failed: {error}");
}
{
let mut member = self.0.member.lock();
*member = membership::MemberState::new();
}
let resolved = self
.0
.drop_partition_state(&KrafkaError::closed("unsubscribed"));
self.0.install_assignment(Vec::new(), Vec::new());
self.0.report(&resolved);
self.0.session_generation.fetch_add(1, Ordering::AcqRel);
self.0.wake_nodes();
debug!(group = %self.0.config.group_id, "unsubscribed");
}
pub async fn close(&self) -> Result<()> {
self.close_with(CloseOptions::default()).await
}
pub async fn close_with(&self, options: CloseOptions) -> Result<()> {
let inner = &self.0;
if inner.closed.swap(true, Ordering::AcqRel) {
return Ok(());
}
let timeout = options.timeout.unwrap_or(DEFAULT_CLOSE_TIMEOUT);
let deadline = tokio::time::Instant::now() + timeout;
inner.records_ready.notify_waiters();
membership::stop(inner);
inner.accept_delivered();
inner.state.lock().closing = true;
inner.wake_nodes();
inner.shut_down.store(true, Ordering::Release);
let nodes: Vec<_> = inner.nodes.lock().drain().map(|(_, h)| h).collect();
let sessions_deadline = deadline - timeout / 4;
for node in nodes {
let mut task = node.task;
if tokio::time::timeout_at(sessions_deadline, &mut task)
.await
.is_err()
{
debug!("a share session close did not finish in time");
task.abort();
}
}
let left = match tokio::time::timeout_at(deadline, membership::leave(inner)).await {
Ok(result) => result,
Err(_) => Err(KrafkaError::timeout("leaving the share group")),
};
let resolved = inner.drop_partition_state(&KrafkaError::closed("share consumer closed"));
inner.report(&resolved);
inner.install_assignment(Vec::new(), Vec::new());
inner
.telemetry
.close(deadline.saturating_duration_since(tokio::time::Instant::now()))
.await;
info!(group = %inner.config.group_id, "share consumer closed");
left
}
#[inline]
pub fn is_closed(&self) -> bool {
self.0.closed.load(Ordering::Acquire)
}
#[inline]
pub fn wakeup(&self) {
self.0.wakeup.store(true, Ordering::Release);
self.0.records_ready.notify_waiters();
}
#[inline]
#[must_use]
pub fn acquisition_lock_timeout(&self) -> Option<Duration> {
let ms = self.0.acquisition_lock_timeout_ms.load(Ordering::Relaxed);
(ms > 0).then(|| Duration::from_millis(ms as u64))
}
pub fn metrics(&self) -> crate::metrics::Metrics {
self.0.metrics_source.snapshot()
}
pub async fn client_instance_id(
&self,
timeout: Duration,
) -> Result<Option<crate::metrics::ClientInstanceId>> {
self.0.telemetry.client_instance_id(timeout).await
}
}