| use crate::client::Client; |
| use crate::consumer::{Consumer, ConsumerKind}; |
| use crate::diagnostic::DiagnosticEvent; |
| use crate::error::IggyError; |
| use crate::identifier::{IdKind, Identifier}; |
| use crate::locking::{IggySharedMut, IggySharedMutFn}; |
| use crate::messages::poll_messages::{PollingKind, PollingStrategy}; |
| use crate::models::messages::{PolledMessage, PolledMessages}; |
| use crate::utils::crypto::Encryptor; |
| use crate::utils::duration::IggyDuration; |
| use crate::utils::timestamp::IggyTimestamp; |
| use bytes::Bytes; |
| use dashmap::DashMap; |
| use futures::Stream; |
| use futures_util::{FutureExt, StreamExt}; |
| use std::collections::VecDeque; |
| use std::future::Future; |
| use std::pin::Pin; |
| use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64}; |
| use std::sync::Arc; |
| use std::task::{Context, Poll}; |
| use std::time::Duration; |
| use tokio::time::sleep; |
| use tracing::{error, info, trace, warn}; |
| |
| const EMPTY_MESSAGES: Vec<PolledMessage> = Vec::new(); |
| |
| const ORDERING: std::sync::atomic::Ordering = std::sync::atomic::Ordering::SeqCst; |
| type PollMessagesFuture = Pin<Box<dyn Future<Output = Result<PolledMessages, IggyError>>>>; |
| |
| /// The auto-commit configuration for storing the offset on the server. |
| #[derive(Debug, PartialEq, Copy, Clone)] |
| pub enum AutoCommit { |
| /// The auto-commit is disabled and the offset must be stored manually by the consumer. |
| Disabled, |
| /// The auto-commit is enabled and the offset is stored on the server after a certain interval. |
| Interval(IggyDuration), |
| /// The auto-commit is enabled and the offset is stored on the server after a certain interval or depending on the mode. |
| IntervalOrWhen(IggyDuration, AutoCommitWhen), |
| /// The auto-commit is enabled and the offset is stored on the server depending on the mode. |
| When(AutoCommitWhen), |
| } |
| |
| /// The auto-commit mode for storing the offset on the server. |
| #[derive(Debug, PartialEq, Copy, Clone)] |
| pub enum AutoCommitWhen { |
| /// The offset is stored on the server when the messages are received. |
| PollingMessages, |
| /// The offset is stored on the server when all the messages are consumed. |
| ConsumingAllMessages, |
| /// The offset is stored on the server when consuming each message. |
| ConsumingEachMessage, |
| /// The offset is stored on the server when consuming every Nth message. |
| ConsumingEveryNthMessage(u32), |
| } |
| |
| unsafe impl Send for IggyConsumer {} |
| unsafe impl Sync for IggyConsumer {} |
| |
| pub struct IggyConsumer { |
| initialized: bool, |
| can_poll: Arc<AtomicBool>, |
| client: IggySharedMut<Box<dyn Client>>, |
| consumer_name: String, |
| consumer: Arc<Consumer>, |
| is_consumer_group: bool, |
| joined_consumer_group: Arc<AtomicBool>, |
| stream_id: Arc<Identifier>, |
| topic_id: Arc<Identifier>, |
| partition_id: Option<u32>, |
| polling_strategy: PollingStrategy, |
| poll_interval_micros: u64, |
| batch_size: u32, |
| auto_commit: AutoCommit, |
| auto_commit_after_polling: bool, |
| auto_join_consumer_group: bool, |
| create_consumer_group_if_not_exists: bool, |
| last_stored_offsets: Arc<DashMap<u32, AtomicU64>>, |
| last_consumed_offsets: Arc<DashMap<u32, AtomicU64>>, |
| current_offsets: Arc<DashMap<u32, AtomicU64>>, |
| poll_future: Option<PollMessagesFuture>, |
| buffered_messages: VecDeque<PolledMessage>, |
| encryptor: Option<Arc<dyn Encryptor>>, |
| store_offset_sender: flume::Sender<(u32, u64)>, |
| store_offset_after_each_message: bool, |
| store_offset_after_all_messages: bool, |
| store_after_every_nth_message: u64, |
| last_polled_at: Arc<AtomicU64>, |
| current_partition_id: Arc<AtomicU32>, |
| retry_interval: IggyDuration, |
| } |
| |
| impl IggyConsumer { |
| #[allow(clippy::too_many_arguments)] |
| pub(crate) fn new( |
| client: IggySharedMut<Box<dyn Client>>, |
| consumer_name: String, |
| consumer: Consumer, |
| stream_id: Identifier, |
| topic_id: Identifier, |
| partition_id: Option<u32>, |
| polling_interval: Option<IggyDuration>, |
| polling_strategy: PollingStrategy, |
| batch_size: u32, |
| auto_commit: AutoCommit, |
| auto_join_consumer_group: bool, |
| create_consumer_group_if_not_exists: bool, |
| encryptor: Option<Arc<dyn Encryptor>>, |
| retry_interval: IggyDuration, |
| ) -> Self { |
| let (store_offset_sender, _) = flume::unbounded(); |
| Self { |
| initialized: false, |
| is_consumer_group: consumer.kind == ConsumerKind::ConsumerGroup, |
| joined_consumer_group: Arc::new(AtomicBool::new(false)), |
| can_poll: Arc::new(AtomicBool::new(true)), |
| client, |
| consumer_name, |
| consumer: Arc::new(consumer), |
| stream_id: Arc::new(stream_id), |
| topic_id: Arc::new(topic_id), |
| partition_id, |
| polling_strategy, |
| poll_interval_micros: polling_interval.map_or(0, |interval| interval.as_micros()), |
| last_stored_offsets: Arc::new(DashMap::new()), |
| last_consumed_offsets: Arc::new(DashMap::new()), |
| current_offsets: Arc::new(DashMap::new()), |
| poll_future: None, |
| batch_size, |
| auto_commit, |
| auto_commit_after_polling: matches!( |
| auto_commit, |
| AutoCommit::When(AutoCommitWhen::PollingMessages) |
| | AutoCommit::IntervalOrWhen(_, AutoCommitWhen::PollingMessages) |
| ), |
| auto_join_consumer_group, |
| create_consumer_group_if_not_exists, |
| buffered_messages: VecDeque::new(), |
| encryptor, |
| store_offset_sender, |
| store_offset_after_each_message: matches!( |
| auto_commit, |
| AutoCommit::When(AutoCommitWhen::ConsumingEachMessage) |
| | AutoCommit::IntervalOrWhen(_, AutoCommitWhen::ConsumingEachMessage) |
| ), |
| store_offset_after_all_messages: matches!( |
| auto_commit, |
| AutoCommit::When(AutoCommitWhen::ConsumingAllMessages) |
| | AutoCommit::IntervalOrWhen(_, AutoCommitWhen::ConsumingAllMessages) |
| ), |
| store_after_every_nth_message: match auto_commit { |
| AutoCommit::When(AutoCommitWhen::ConsumingEveryNthMessage(n)) |
| | AutoCommit::IntervalOrWhen(_, AutoCommitWhen::ConsumingEveryNthMessage(n)) => { |
| n as u64 |
| } |
| _ => 0, |
| }, |
| last_polled_at: Arc::new(AtomicU64::new(0)), |
| current_partition_id: Arc::new(AtomicU32::new(0)), |
| retry_interval, |
| } |
| } |
| |
| pub fn stream(&self) -> &Identifier { |
| &self.stream_id |
| } |
| |
| pub fn topic(&self) -> &Identifier { |
| &self.topic_id |
| } |
| |
| /// Initializes the consumer by subscribing to diagnostic events, initializing the consumer group if needed, storing the offsets in the background etc. |
| pub async fn init(&mut self) -> Result<(), IggyError> { |
| if self.initialized { |
| return Ok(()); |
| } |
| |
| { |
| let client = self.client.read().await; |
| if client.get_stream(&self.stream_id).await?.is_none() { |
| return Err(IggyError::StreamNameNotFound( |
| self.stream_id.get_string_value().unwrap_or_default(), |
| )); |
| } |
| |
| if client |
| .get_topic(&self.stream_id, &self.topic_id) |
| .await? |
| .is_none() |
| { |
| return Err(IggyError::TopicNameNotFound( |
| self.topic_id.get_string_value().unwrap_or_default(), |
| self.stream_id.get_string_value().unwrap_or_default(), |
| )); |
| } |
| } |
| |
| self.subscribe_events().await; |
| self.init_consumer_group().await?; |
| |
| match self.auto_commit { |
| AutoCommit::Interval(interval) => self.store_offsets_in_background(interval), |
| AutoCommit::IntervalOrWhen(interval, _) => self.store_offsets_in_background(interval), |
| _ => {} |
| } |
| |
| let client = self.client.clone(); |
| let consumer = self.consumer.clone(); |
| let stream_id = self.stream_id.clone(); |
| let topic_id = self.topic_id.clone(); |
| let last_stored_offset = self.last_stored_offsets.clone(); |
| let (store_offset_sender, store_offset_receiver) = flume::unbounded(); |
| self.store_offset_sender = store_offset_sender; |
| |
| tokio::spawn(async move { |
| while let Ok((partition_id, offset)) = store_offset_receiver.recv_async().await { |
| trace!("Received offset to store: {offset}, partition ID: {partition_id}"); |
| let stored_offset; |
| if let Some(offset_entry) = last_stored_offset.get(&partition_id) { |
| stored_offset = offset_entry.value().load(ORDERING); |
| } else { |
| stored_offset = 0; |
| last_stored_offset.insert(partition_id, AtomicU64::new(0)); |
| } |
| |
| if offset <= stored_offset && offset >= 1 { |
| trace!("Offset: {offset} is less than or equal to the last stored offset: {stored_offset}. Skipping storing the offset."); |
| continue; |
| } |
| |
| let client = client.read().await; |
| if let Err(error) = client |
| .store_consumer_offset( |
| &consumer, |
| &stream_id, |
| &topic_id, |
| Some(partition_id), |
| offset, |
| ) |
| .await |
| { |
| error!("Failed to store offset: {offset}, error: {error}"); |
| continue; |
| } |
| trace!("Stored offset: {offset}"); |
| if let Some(last_offset_entry) = last_stored_offset.get(&partition_id) { |
| last_offset_entry.value().store(offset, ORDERING); |
| } else { |
| last_stored_offset.insert(partition_id, AtomicU64::new(offset)); |
| } |
| } |
| }); |
| |
| self.initialized = true; |
| Ok(()) |
| } |
| |
| fn store_offset(&self, partition_id: u32, offset: u64) { |
| if let Err(error) = self.store_offset_sender.send((partition_id, offset)) { |
| error!("Failed to send offset to store: {error}"); |
| } |
| } |
| |
| fn store_offsets_in_background(&self, interval: IggyDuration) { |
| let client = self.client.clone(); |
| let consumer = self.consumer.clone(); |
| let stream_id = self.stream_id.clone(); |
| let topic_id = self.topic_id.clone(); |
| let last_consumed_offset = self.last_consumed_offsets.clone(); |
| let last_stored_offset = self.last_stored_offsets.clone(); |
| tokio::spawn(async move { |
| loop { |
| sleep(interval.get_duration()).await; |
| for entry in last_consumed_offset.iter() { |
| let partition_id = *entry.key(); |
| let consumed_offset = entry.value().load(ORDERING); |
| let stored_offset = last_stored_offset |
| .get(&partition_id) |
| .map_or(0, |offset| offset.load(ORDERING)); |
| trace!( |
| "Trying to store the offset: {consumed_offset}, last stored offset: {stored_offset} for partition ID: {partition_id}" |
| ); |
| |
| if consumed_offset <= stored_offset && consumed_offset >= 1 { |
| trace!("Offset: {consumed_offset} is less than or equal to the last stored offset: {stored_offset} for partition ID: {partition_id}. Skipping storing the offset in the background."); |
| continue; |
| } |
| |
| let client = client.read().await; |
| if let Err(error) = client |
| .store_consumer_offset( |
| &consumer, |
| &stream_id, |
| &topic_id, |
| Some(partition_id), |
| consumed_offset, |
| ) |
| .await |
| { |
| error!( |
| "Failed to store offset: {consumed_offset} for partition ID: {partition_id} in the background, error: {error}" |
| ); |
| continue; |
| } |
| trace!("Stored offset: {consumed_offset} for partition ID: {partition_id} in the background."); |
| if let Some(stored_offset) = last_stored_offset.get(&partition_id) { |
| stored_offset.store(consumed_offset, ORDERING); |
| } else { |
| last_stored_offset.insert(partition_id, AtomicU64::new(consumed_offset)); |
| } |
| } |
| } |
| }); |
| } |
| |
| async fn init_consumer_group(&self) -> Result<(), IggyError> { |
| if !self.is_consumer_group { |
| return Ok(()); |
| } |
| |
| if !self.auto_join_consumer_group { |
| warn!("Auto join consumer group is disabled"); |
| return Ok(()); |
| } |
| |
| Self::initialize_consumer_group( |
| self.client.clone(), |
| self.create_consumer_group_if_not_exists, |
| self.stream_id.clone(), |
| self.topic_id.clone(), |
| self.consumer.clone(), |
| &self.consumer_name, |
| self.joined_consumer_group.clone(), |
| ) |
| .await |
| } |
| |
| async fn subscribe_events(&self) { |
| trace!("Subscribing to diagnostic events"); |
| let mut receiver; |
| { |
| let client = self.client.read().await; |
| receiver = client.subscribe_events().await; |
| } |
| |
| let is_consumer_group = self.is_consumer_group; |
| let can_join_consumer_group = is_consumer_group && self.auto_join_consumer_group; |
| let client = self.client.clone(); |
| let create_consumer_group_if_not_exists = self.create_consumer_group_if_not_exists; |
| let stream_id = self.stream_id.clone(); |
| let topic_id = self.topic_id.clone(); |
| let consumer = self.consumer.clone(); |
| let consumer_name = self.consumer_name.clone(); |
| let can_poll = self.can_poll.clone(); |
| let joined_consumer_group = self.joined_consumer_group.clone(); |
| let mut reconnected = false; |
| let mut disconnected = false; |
| |
| tokio::spawn(async move { |
| while let Some(event) = receiver.next().await { |
| trace!("Received diagnostic event: {event}"); |
| match event { |
| DiagnosticEvent::Connected => { |
| trace!("Connected to the server"); |
| joined_consumer_group.store(false, ORDERING); |
| if disconnected { |
| reconnected = true; |
| disconnected = false; |
| } |
| } |
| DiagnosticEvent::Disconnected => { |
| disconnected = true; |
| reconnected = false; |
| joined_consumer_group.store(false, ORDERING); |
| can_poll.store(false, ORDERING); |
| warn!("Disconnected from the server"); |
| } |
| DiagnosticEvent::SignedIn => { |
| if !is_consumer_group { |
| can_poll.store(true, ORDERING); |
| continue; |
| } |
| |
| if !can_join_consumer_group { |
| can_poll.store(true, ORDERING); |
| trace!("Auto join consumer group is disabled"); |
| continue; |
| } |
| |
| if !reconnected { |
| can_poll.store(true, ORDERING); |
| continue; |
| } |
| |
| if joined_consumer_group.load(ORDERING) { |
| can_poll.store(true, ORDERING); |
| continue; |
| } |
| |
| info!("Rejoining consumer group"); |
| if let Err(error) = Self::initialize_consumer_group( |
| client.clone(), |
| create_consumer_group_if_not_exists, |
| stream_id.clone(), |
| topic_id.clone(), |
| consumer.clone(), |
| &consumer_name, |
| joined_consumer_group.clone(), |
| ) |
| .await |
| { |
| error!("Failed to join consumer group: {error}"); |
| continue; |
| } |
| info!("Rejoined consumer group"); |
| can_poll.store(true, ORDERING); |
| } |
| DiagnosticEvent::SignedOut => { |
| joined_consumer_group.store(false, ORDERING); |
| can_poll.store(false, ORDERING); |
| } |
| } |
| } |
| }); |
| } |
| |
| fn create_poll_messages_future( |
| &self, |
| ) -> impl Future<Output = Result<PolledMessages, IggyError>> { |
| let stream_id = self.stream_id.clone(); |
| let topic_id = self.topic_id.clone(); |
| let partition_id = self.partition_id; |
| let consumer = self.consumer.clone(); |
| let polling_strategy = self.polling_strategy; |
| let client = self.client.clone(); |
| let count = self.batch_size; |
| let auto_commit_after_polling = self.auto_commit_after_polling; |
| let auto_commit_enabled = self.auto_commit != AutoCommit::Disabled; |
| let interval = self.poll_interval_micros; |
| let last_polled_at = self.last_polled_at.clone(); |
| let can_poll = self.can_poll.clone(); |
| let retry_interval = self.retry_interval; |
| let last_stored_offset = self.last_stored_offsets.clone(); |
| let last_consumed_offset = self.last_consumed_offsets.clone(); |
| |
| async move { |
| if interval > 0 { |
| Self::wait_before_polling(interval, last_polled_at.load(ORDERING)).await; |
| } |
| |
| if !can_poll.load(ORDERING) { |
| trace!("Trying to poll messages in {retry_interval}..."); |
| sleep(retry_interval.get_duration()).await; |
| } |
| |
| trace!("Sending poll messages request"); |
| last_polled_at.store(IggyTimestamp::now().into(), ORDERING); |
| let polled_messages = client |
| .read() |
| .await |
| .poll_messages( |
| &stream_id, |
| &topic_id, |
| partition_id, |
| &consumer, |
| &polling_strategy, |
| count, |
| auto_commit_after_polling, |
| ) |
| .await; |
| |
| if let Ok(polled_messages) = polled_messages { |
| if polled_messages.messages.is_empty() { |
| return Ok(polled_messages); |
| } |
| |
| let partition_id = polled_messages.partition_id; |
| let consumed_offset; |
| let has_consumed_offset; |
| if let Some(offset_entry) = last_consumed_offset.get(&partition_id) { |
| has_consumed_offset = true; |
| consumed_offset = offset_entry.value().load(ORDERING); |
| } else { |
| consumed_offset = 0; |
| has_consumed_offset = false; |
| last_consumed_offset.insert(partition_id, AtomicU64::new(0)); |
| } |
| |
| if has_consumed_offset && consumed_offset >= polled_messages.messages[0].offset { |
| return Ok(PolledMessages { |
| messages: EMPTY_MESSAGES, |
| current_offset: polled_messages.current_offset, |
| partition_id, |
| }); |
| } |
| |
| let stored_offset; |
| if let Some(stored_offset_entry) = last_stored_offset.get(&partition_id) { |
| if auto_commit_after_polling { |
| stored_offset_entry.store(consumed_offset, ORDERING); |
| stored_offset = consumed_offset; |
| } else { |
| stored_offset = stored_offset_entry.value().load(ORDERING); |
| } |
| } else { |
| if auto_commit_after_polling { |
| stored_offset = consumed_offset; |
| } else { |
| stored_offset = 0; |
| } |
| last_stored_offset.insert(partition_id, AtomicU64::new(stored_offset)); |
| } |
| |
| trace!( |
| "Last consumed offset: {consumed_offset}, current offset: {}, stored offset: {stored_offset}, in partition ID: {partition_id}, topic: {topic_id}, stream: {stream_id}, consumer: {consumer}", |
| polled_messages.current_offset |
| ); |
| |
| if has_consumed_offset && polled_messages.current_offset == consumed_offset { |
| trace!("No new messages to consume in partition ID: {partition_id}, topic: {topic_id}, stream: {stream_id}, consumer: {consumer}"); |
| if auto_commit_enabled && stored_offset < consumed_offset { |
| trace!("Auto-committing the offset: {consumed_offset} in partition ID: {partition_id}, topic: {topic_id}, stream: {stream_id}, consumer: {consumer}"); |
| client |
| .read() |
| .await |
| .store_consumer_offset( |
| &consumer, |
| &stream_id, |
| &topic_id, |
| Some(partition_id), |
| consumed_offset, |
| ) |
| .await?; |
| if let Some(stored_offset_entry) = last_stored_offset.get(&partition_id) { |
| stored_offset_entry.store(consumed_offset, ORDERING); |
| } else { |
| last_stored_offset |
| .insert(partition_id, AtomicU64::new(consumed_offset)); |
| } |
| } |
| |
| return Ok(PolledMessages { |
| messages: EMPTY_MESSAGES, |
| current_offset: polled_messages.current_offset, |
| partition_id, |
| }); |
| } |
| |
| return Ok(polled_messages); |
| } |
| |
| let error = polled_messages.unwrap_err(); |
| error!("Failed to poll messages: {error}"); |
| if matches!(error, IggyError::Disconnected | IggyError::Unauthenticated) { |
| trace!("Retrying to poll messages in {retry_interval}..."); |
| sleep(retry_interval.get_duration()).await; |
| } |
| Err(error) |
| } |
| } |
| |
| async fn wait_before_polling(interval: u64, last_sent_at: u64) { |
| if interval == 0 { |
| return; |
| } |
| |
| let now: u64 = IggyTimestamp::now().into(); |
| let elapsed = now - last_sent_at; |
| if elapsed >= interval { |
| trace!("No need to wait before polling messages. {now} - {last_sent_at} = {elapsed}"); |
| return; |
| } |
| |
| let remaining = interval - elapsed; |
| trace!("Waiting for {remaining} microseconds before polling messages... {interval} - {elapsed} = {remaining}"); |
| sleep(Duration::from_micros(remaining)).await; |
| } |
| |
| async fn initialize_consumer_group( |
| client: IggySharedMut<Box<dyn Client>>, |
| create_consumer_group_if_not_exists: bool, |
| stream_id: Arc<Identifier>, |
| topic_id: Arc<Identifier>, |
| consumer: Arc<Consumer>, |
| consumer_name: &str, |
| joined_consumer_group: Arc<AtomicBool>, |
| ) -> Result<(), IggyError> { |
| if joined_consumer_group.load(ORDERING) { |
| return Ok(()); |
| } |
| |
| let client = client.read().await; |
| let (name, id) = match consumer.id.kind { |
| IdKind::Numeric => (consumer_name.to_owned(), Some(consumer.id.get_u32_value()?)), |
| IdKind::String => (consumer.id.get_string_value()?, None), |
| }; |
| |
| let consumer_group_id = name.to_owned().try_into()?; |
| trace!("Validating consumer group: {consumer_group_id} for topic: {topic_id}, stream: {stream_id}"); |
| if client |
| .get_consumer_group(&stream_id, &topic_id, &consumer_group_id) |
| .await? |
| .is_none() |
| { |
| if !create_consumer_group_if_not_exists { |
| error!("Consumer group does not exist and auto-creation is disabled."); |
| return Err(IggyError::ConsumerGroupNameNotFound( |
| name.to_owned(), |
| topic_id.get_string_value().unwrap_or_default(), |
| )); |
| } |
| |
| info!("Creating consumer group: {consumer_group_id} for topic: {topic_id}, stream: {stream_id}"); |
| client |
| .create_consumer_group(&stream_id, &topic_id, &name, id) |
| .await?; |
| } |
| |
| info!("Joining consumer group: {consumer_group_id} for topic: {topic_id}, stream: {stream_id}",); |
| if let Err(error) = client |
| .join_consumer_group(&stream_id, &topic_id, &consumer_group_id) |
| .await |
| { |
| joined_consumer_group.store(false, ORDERING); |
| error!("Failed to join consumer group: {consumer_group_id} for topic: {topic_id}, stream: {stream_id}: {error}"); |
| return Err(error); |
| } |
| |
| joined_consumer_group.store(true, ORDERING); |
| info!( |
| "Joined consumer group: {consumer_group_id} for topic: {topic_id}, stream: {stream_id}" |
| ); |
| Ok(()) |
| } |
| } |
| |
| pub struct ReceivedMessage { |
| pub message: PolledMessage, |
| pub current_offset: u64, |
| pub partition_id: u32, |
| } |
| |
| impl ReceivedMessage { |
| pub fn new(message: PolledMessage, current_offset: u64, partition_id: u32) -> Self { |
| Self { |
| message, |
| current_offset, |
| partition_id, |
| } |
| } |
| } |
| |
| impl Stream for IggyConsumer { |
| type Item = Result<ReceivedMessage, IggyError>; |
| |
| fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { |
| let partition_id = self.current_partition_id.load(ORDERING); |
| if let Some(message) = self.buffered_messages.pop_front() { |
| { |
| if let Some(last_consumed_offset_entry) = |
| self.last_consumed_offsets.get(&partition_id) |
| { |
| last_consumed_offset_entry.store(message.offset, ORDERING); |
| } else { |
| self.last_consumed_offsets |
| .insert(partition_id, AtomicU64::new(message.offset)); |
| } |
| |
| if (self.store_after_every_nth_message > 0 |
| && message.offset % self.store_after_every_nth_message == 0) |
| || self.store_offset_after_each_message |
| { |
| self.store_offset(partition_id, message.offset); |
| } |
| } |
| |
| if self.buffered_messages.is_empty() { |
| if self.polling_strategy.kind == PollingKind::Offset { |
| self.polling_strategy = PollingStrategy::offset(message.offset + 1); |
| } |
| |
| if self.store_offset_after_all_messages { |
| self.store_offset(partition_id, message.offset); |
| } |
| } |
| |
| let current_offset; |
| if let Some(current_offset_entry) = self.current_offsets.get(&partition_id) { |
| current_offset = current_offset_entry.load(ORDERING); |
| } else { |
| current_offset = 0; |
| } |
| |
| return Poll::Ready(Some(Ok(ReceivedMessage::new( |
| message, |
| current_offset, |
| partition_id, |
| )))); |
| } |
| |
| if self.poll_future.is_none() { |
| let future = self.create_poll_messages_future(); |
| self.poll_future = Some(Box::pin(future)); |
| } |
| |
| while let Some(future) = self.poll_future.as_mut() { |
| match future.poll_unpin(cx) { |
| Poll::Ready(Ok(mut polled_messages)) => { |
| let partition_id = polled_messages.partition_id; |
| self.current_partition_id.store(partition_id, ORDERING); |
| if polled_messages.messages.is_empty() { |
| self.poll_future = Some(Box::pin(self.create_poll_messages_future())); |
| } else { |
| if let Some(ref encryptor) = self.encryptor { |
| for message in &mut polled_messages.messages { |
| let payload = encryptor.decrypt(&message.payload); |
| if payload.is_err() { |
| self.poll_future = None; |
| error!("Failed to decrypt the message payload at offset: {}, partition ID: {}", message.offset, partition_id); |
| let error = payload.unwrap_err(); |
| return Poll::Ready(Some(Err(error))); |
| } |
| |
| let payload = payload.unwrap(); |
| message.payload = Bytes::from(payload); |
| message.length = message.payload.len() as u32; |
| } |
| } |
| |
| if let Some(current_offset_entry) = self.current_offsets.get(&partition_id) |
| { |
| current_offset_entry.store(polled_messages.current_offset, ORDERING); |
| } else { |
| self.current_offsets.insert( |
| partition_id, |
| AtomicU64::new(polled_messages.current_offset), |
| ); |
| } |
| |
| let message = polled_messages.messages.remove(0); |
| self.buffered_messages.extend(polled_messages.messages); |
| |
| if self.polling_strategy.kind == PollingKind::Offset { |
| self.polling_strategy = PollingStrategy::offset(message.offset + 1); |
| } |
| |
| if let Some(last_consumed_offset_entry) = |
| self.last_consumed_offsets.get(&partition_id) |
| { |
| last_consumed_offset_entry.store(message.offset, ORDERING); |
| } else { |
| self.last_consumed_offsets |
| .insert(partition_id, AtomicU64::new(message.offset)); |
| } |
| |
| if (self.store_after_every_nth_message > 0 |
| && message.offset % self.store_after_every_nth_message == 0) |
| || self.store_offset_after_each_message |
| || (self.store_offset_after_all_messages |
| && self.buffered_messages.is_empty()) |
| { |
| self.store_offset(polled_messages.partition_id, message.offset); |
| } |
| |
| self.poll_future = None; |
| return Poll::Ready(Some(Ok(ReceivedMessage::new( |
| message, |
| polled_messages.current_offset, |
| polled_messages.partition_id, |
| )))); |
| } |
| } |
| Poll::Ready(Err(err)) => { |
| self.poll_future = None; |
| return Poll::Ready(Some(Err(err))); |
| } |
| Poll::Pending => return Poll::Pending, |
| } |
| } |
| |
| Poll::Pending |
| } |
| } |
| |
| #[derive(Debug)] |
| pub struct IggyConsumerBuilder { |
| client: IggySharedMut<Box<dyn Client>>, |
| consumer_name: String, |
| consumer: Consumer, |
| stream: Identifier, |
| topic: Identifier, |
| partition: Option<u32>, |
| polling_strategy: PollingStrategy, |
| polling_interval: Option<IggyDuration>, |
| batch_size: u32, |
| auto_commit: AutoCommit, |
| auto_join_consumer_group: bool, |
| create_consumer_group_if_not_exists: bool, |
| encryptor: Option<Arc<dyn Encryptor>>, |
| retry_interval: IggyDuration, |
| } |
| |
| impl IggyConsumerBuilder { |
| #[allow(clippy::too_many_arguments)] |
| pub(crate) fn new( |
| client: IggySharedMut<Box<dyn Client>>, |
| consumer_name: String, |
| consumer: Consumer, |
| stream_id: Identifier, |
| topic_id: Identifier, |
| partition_id: Option<u32>, |
| encryptor: Option<Arc<dyn Encryptor>>, |
| polling_interval: Option<IggyDuration>, |
| ) -> Self { |
| Self { |
| client, |
| consumer_name, |
| consumer, |
| stream: stream_id, |
| topic: topic_id, |
| partition: partition_id, |
| polling_strategy: PollingStrategy::next(), |
| batch_size: 1000, |
| auto_commit: AutoCommit::IntervalOrWhen( |
| IggyDuration::ONE_SECOND, |
| AutoCommitWhen::PollingMessages, |
| ), |
| auto_join_consumer_group: true, |
| create_consumer_group_if_not_exists: true, |
| encryptor, |
| polling_interval, |
| retry_interval: IggyDuration::ONE_SECOND, |
| } |
| } |
| |
| /// Sets the stream identifier. |
| pub fn stream(self, stream: Identifier) -> Self { |
| Self { stream, ..self } |
| } |
| |
| /// Sets the topic identifier. |
| pub fn topic(self, topic: Identifier) -> Self { |
| Self { topic, ..self } |
| } |
| |
| /// Sets the partition identifier. |
| pub fn partition(self, partition: Option<u32>) -> Self { |
| Self { partition, ..self } |
| } |
| |
| /// Sets the polling strategy. |
| pub fn polling_strategy(self, polling_strategy: PollingStrategy) -> Self { |
| Self { |
| polling_strategy, |
| ..self |
| } |
| } |
| |
| /// Sets the batch size for polling messages. |
| pub fn batch_size(self, batch_size: u32) -> Self { |
| Self { batch_size, ..self } |
| } |
| |
| /// Sets the auto-commit configuration for storing the offset on the server. |
| pub fn auto_commit(self, auto_commit: AutoCommit) -> Self { |
| Self { |
| auto_commit, |
| ..self |
| } |
| } |
| |
| /// Automatically joins the consumer group if the consumer is a part of a consumer group. |
| pub fn auto_join_consumer_group(self) -> Self { |
| Self { |
| auto_join_consumer_group: true, |
| ..self |
| } |
| } |
| |
| /// Does not automatically join the consumer group if the consumer is a part of a consumer group. |
| pub fn do_not_auto_join_consumer_group(self) -> Self { |
| Self { |
| auto_join_consumer_group: false, |
| ..self |
| } |
| } |
| |
| /// Automatically creates the consumer group if it does not exist. |
| pub fn create_consumer_group_if_not_exists(self) -> Self { |
| Self { |
| create_consumer_group_if_not_exists: true, |
| ..self |
| } |
| } |
| |
| /// Does not automatically create the consumer group if it does not exist. |
| pub fn do_not_create_consumer_group_if_not_exists(self) -> Self { |
| Self { |
| create_consumer_group_if_not_exists: false, |
| ..self |
| } |
| } |
| |
| /// Sets the polling interval for messages. |
| pub fn poll_interval(self, interval: IggyDuration) -> Self { |
| Self { |
| polling_interval: Some(interval), |
| ..self |
| } |
| } |
| |
| /// Clears the polling interval for messages. |
| pub fn without_poll_interval(self) -> Self { |
| Self { |
| polling_interval: None, |
| ..self |
| } |
| } |
| |
| /// Sets the encryptor for decrypting the messages' payloads. |
| pub fn encryptor(self, encryptor: Arc<dyn Encryptor>) -> Self { |
| Self { |
| encryptor: Some(encryptor), |
| ..self |
| } |
| } |
| |
| /// Clears the encryptor for decrypting the messages' payloads. |
| pub fn without_encryptor(self) -> Self { |
| Self { |
| encryptor: None, |
| ..self |
| } |
| } |
| |
| /// Sets the retry interval in case of server disconnection. |
| pub fn retry_interval(self, interval: IggyDuration) -> Self { |
| Self { |
| retry_interval: interval, |
| ..self |
| } |
| } |
| |
| pub fn build(self) -> IggyConsumer { |
| IggyConsumer::new( |
| self.client, |
| self.consumer_name, |
| self.consumer, |
| self.stream, |
| self.topic, |
| self.partition, |
| self.polling_interval, |
| self.polling_strategy, |
| self.batch_size, |
| self.auto_commit, |
| self.auto_join_consumer_group, |
| self.create_consumer_group_if_not_exists, |
| self.encryptor, |
| self.retry_interval, |
| ) |
| } |
| } |