1use crate::capabilities::{Capabilities, HelloOutcome};
2use crate::error::LaserError;
3use crate::filters::reader::{DEFAULT_IDLE_INTERVAL, Delivery, FilteredReader, MatchedRecord};
4use crate::stream::consumer_group::ConsumerGroup;
5use crate::stream::{HeaderKey, HeaderValue, Topic};
6use crate::types::MessageId;
7use bytes::Bytes;
8use futures::Stream;
9use iggy::prelude::{
10 AutoCommit, AutoCommitWhen, BackgroundConfig, ConsumerGroupClient, ConsumerOffsetClient,
11 DirectConfig, Identifier, IggyConsumer, IggyConsumerBuilder, IggyExpiry, IggyMessage,
12 IggyProducer, IggyTimestamp, MaxTopicSize, Partitioning, PollingStrategy, ReceivedMessage,
13 SendMessagesResponse,
14};
15use laser_wire::filter::{FilterRef, FilteredStart};
16use serde::de::DeserializeOwned;
17use std::collections::{BTreeMap, BTreeSet, VecDeque};
18use std::future::Future;
19use std::pin::Pin;
20use std::sync::Arc;
21use std::task::{Context, Poll};
22use std::time::Duration;
23use tokio::time::{Instant, sleep};
24
25const DEFAULT_BATCH_LENGTH: u32 = 1000;
26const DEFAULT_RETRY_INTERVAL: Duration = Duration::from_secs(1);
27
28pub type Headers = BTreeMap<HeaderKey, HeaderValue>;
30
31#[derive(Debug, Clone, PartialEq, Eq, Default)]
33pub enum Routing {
34 #[default]
36 Balanced,
37 Key(Vec<u8>),
39 Partition(u32),
41}
42
43impl Routing {
44 pub fn key(value: impl Into<Vec<u8>>) -> Self {
46 Self::Key(value.into())
47 }
48
49 fn into_partitioning(self) -> Result<Partitioning, LaserError> {
50 match self {
51 Self::Balanced => Ok(Partitioning::balanced()),
52 Self::Key(key) => Ok(Partitioning::messages_key(&key)?),
53 Self::Partition(partition) => Ok(Partitioning::partition_id(partition)),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum ConsumerStart {
61 First,
63 Last,
65 #[default]
67 Next,
68 Offset(u64),
70 TimestampMicros(u64),
72}
73
74impl ConsumerStart {
75 const fn into_filtered(self) -> FilteredStart {
76 match self {
77 Self::First => FilteredStart::First,
78 Self::Last => FilteredStart::Last,
79 Self::Next => FilteredStart::Next,
80 Self::Offset(offset) => FilteredStart::Offset(offset),
81 Self::TimestampMicros(timestamp) => FilteredStart::Timestamp(timestamp),
82 }
83 }
84
85 fn into_polling(self) -> PollingStrategy {
86 match self {
87 Self::First => PollingStrategy::first(),
88 Self::Last => PollingStrategy::last(),
89 Self::Next => PollingStrategy::next(),
90 Self::Offset(offset) => PollingStrategy::offset(offset),
91 Self::TimestampMicros(timestamp) => {
92 PollingStrategy::timestamp(IggyTimestamp::from(timestamp))
93 }
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum CommitPolicy {
101 Disabled,
104 Interval(Duration),
106 #[default]
112 Polling,
113 IntervalOrPolling(Duration),
115 All,
117 IntervalOrAll(Duration),
119 Each,
121 IntervalOrEach(Duration),
123 Every(u32),
125 IntervalOrEvery(Duration, u32),
127}
128
129impl CommitPolicy {
130 fn into_auto_commit(self) -> Result<AutoCommit, LaserError> {
131 let every = |messages: u32| {
132 if messages == 0 {
133 Err(LaserError::Invalid(
134 "commit frequency must be greater than zero".to_owned(),
135 ))
136 } else {
137 Ok(AutoCommitWhen::ConsumingEveryNthMessage(messages))
138 }
139 };
140 Ok(match self {
141 Self::Disabled => AutoCommit::Disabled,
142 Self::Interval(duration) => AutoCommit::Interval(positive_duration(
143 duration,
144 "commit interval must be greater than zero",
145 )?),
146 Self::Polling => AutoCommit::When(AutoCommitWhen::PollingMessages),
147 Self::IntervalOrPolling(duration) => AutoCommit::IntervalOrWhen(
148 positive_duration(duration, "commit interval must be greater than zero")?,
149 AutoCommitWhen::PollingMessages,
150 ),
151 Self::All => AutoCommit::When(AutoCommitWhen::ConsumingAllMessages),
152 Self::IntervalOrAll(duration) => AutoCommit::IntervalOrWhen(
153 positive_duration(duration, "commit interval must be greater than zero")?,
154 AutoCommitWhen::ConsumingAllMessages,
155 ),
156 Self::Each => AutoCommit::When(AutoCommitWhen::ConsumingEachMessage),
157 Self::IntervalOrEach(duration) => AutoCommit::IntervalOrWhen(
158 positive_duration(duration, "commit interval must be greater than zero")?,
159 AutoCommitWhen::ConsumingEachMessage,
160 ),
161 Self::Every(messages) => AutoCommit::When(every(messages)?),
162 Self::IntervalOrEvery(duration, messages) => AutoCommit::IntervalOrWhen(
163 positive_duration(duration, "commit interval must be greater than zero")?,
164 every(messages)?,
165 ),
166 })
167 }
168}
169
170fn positive_duration<T>(duration: Duration, message: &'static str) -> Result<T, LaserError>
171where
172 T: TryFrom<Duration>,
173{
174 if duration.is_zero() {
175 return Err(LaserError::Invalid(message.to_owned()));
176 }
177 duration
178 .try_into()
179 .map_err(|_| LaserError::Invalid(message.to_owned()))
180}
181
182#[derive(Debug, bon::Builder)]
184pub struct ProducerMessage {
185 #[builder(into)]
186 payload: Bytes,
187 #[builder(default)]
188 headers: Headers,
189}
190
191impl ProducerMessage {
192 pub fn new(payload: impl Into<Bytes>) -> Self {
194 Self {
195 payload: payload.into(),
196 headers: Headers::new(),
197 }
198 }
199
200 #[must_use]
202 pub fn with_headers(mut self, headers: Headers) -> Self {
203 self.headers = headers;
204 self
205 }
206
207 #[must_use]
209 pub fn header(mut self, key: HeaderKey, value: HeaderValue) -> Self {
210 self.headers.insert(key, value);
211 self
212 }
213
214 fn into_iggy(self) -> Result<IggyMessage, LaserError> {
215 Ok(IggyMessage::builder()
216 .payload(self.payload)
217 .user_headers(self.headers)
218 .build()?)
219 }
220}
221
222pub struct ProducerBuilder {
224 topic: Topic,
225 batch_length: u32,
226 linger: Duration,
227 retries: Option<u32>,
228 retry_interval: Option<Duration>,
229 routing: Routing,
230 create_stream: bool,
231 create_topic: bool,
232 partitions: u32,
233 expire_after: Option<Duration>,
234 never_expire: bool,
235 max_topic_bytes: Option<u64>,
236 unlimited_topic_size: bool,
237 background: Option<BackgroundConfig>,
238}
239
240impl ProducerBuilder {
241 pub(crate) fn new(topic: Topic) -> Self {
242 let publish = topic.laser.publish_options();
243 Self {
244 topic,
245 batch_length: DEFAULT_BATCH_LENGTH,
246 linger: Duration::ZERO,
247 retries: Some(publish.max_retries),
248 retry_interval: Some(publish.retry_backoff),
249 routing: Routing::Balanced,
250 create_stream: false,
254 create_topic: true,
255 partitions: 1,
256 expire_after: None,
257 never_expire: false,
258 max_topic_bytes: None,
259 unlimited_topic_size: false,
260 background: None,
261 }
262 }
263
264 #[must_use]
265 pub fn batch_length(mut self, batch_length: u32) -> Self {
266 self.batch_length = batch_length;
267 self
268 }
269
270 #[must_use]
271 pub fn linger(mut self, linger: Duration) -> Self {
272 self.linger = linger;
273 self
274 }
275
276 #[must_use]
277 pub fn retries(mut self, retries: Option<u32>, interval: Option<Duration>) -> Self {
278 self.retries = retries;
279 self.retry_interval = interval;
280 self
281 }
282
283 #[must_use]
284 pub fn retry_backoff(mut self, interval: Duration) -> Self {
285 self.retry_interval = Some(interval);
286 self
287 }
288
289 #[must_use]
290 pub fn routing(mut self, routing: Routing) -> Self {
291 self.routing = routing;
292 self
293 }
294
295 #[must_use]
299 pub fn create_stream(mut self, create: bool) -> Self {
300 self.create_stream = create;
301 self
302 }
303
304 #[must_use]
305 pub fn create_topic(mut self, create: bool) -> Self {
306 self.create_topic = create;
307 self
308 }
309
310 #[must_use]
311 pub fn partitions(mut self, partitions: u32) -> Self {
312 self.partitions = partitions;
313 self
314 }
315
316 #[must_use]
321 pub fn expire_after(mut self, expiry: Duration) -> Self {
322 self.expire_after = Some(expiry);
323 self
324 }
325
326 #[must_use]
329 pub fn never_expire(mut self) -> Self {
330 self.never_expire = true;
331 self
332 }
333
334 #[must_use]
339 pub fn max_topic_bytes(mut self, payload: u64) -> Self {
340 self.max_topic_bytes = Some(payload);
341 self
342 }
343
344 #[must_use]
347 pub fn unlimited_topic_size(mut self) -> Self {
348 self.unlimited_topic_size = true;
349 self
350 }
351
352 fn topic_limits(&self) -> Result<(IggyExpiry, MaxTopicSize), LaserError> {
355 let expiry = match (self.expire_after, self.never_expire) {
356 (Some(_), true) => {
357 return Err(LaserError::Invalid(
358 "producer takes expire_after or never_expire, not both".to_owned(),
359 ));
360 }
361 (Some(age), false) if age.is_zero() => {
362 return Err(LaserError::Invalid(
363 "producer expire_after must be greater than zero".to_owned(),
364 ));
365 }
366 (Some(age), false) => IggyExpiry::ExpireDuration(age.into()),
367 (None, true) => IggyExpiry::NeverExpire,
368 (None, false) => IggyExpiry::ServerDefault,
369 };
370 let max_topic_size = match (self.max_topic_bytes, self.unlimited_topic_size) {
371 (Some(_), true) => {
372 return Err(LaserError::Invalid(
373 "producer takes max_topic_bytes or unlimited_topic_size, not both".to_owned(),
374 ));
375 }
376 (Some(0), false) => {
377 return Err(LaserError::Invalid(
378 "producer max_topic_bytes must be greater than zero".to_owned(),
379 ));
380 }
381 (Some(bytes), false) => MaxTopicSize::from(bytes),
382 (None, true) => MaxTopicSize::Unlimited,
383 (None, false) => MaxTopicSize::ServerDefault,
384 };
385 Ok((expiry, max_topic_size))
386 }
387
388 #[must_use]
395 pub fn background(mut self, config: BackgroundConfig) -> Self {
396 self.background = Some(config);
397 self
398 }
399
400 pub async fn build(self) -> Result<Producer, LaserError> {
401 let (expiry, max_topic_size) = self.topic_limits()?;
402 if self.background.is_none() && self.batch_length == 0 {
403 return Err(LaserError::Invalid(
404 "producer batch length must be greater than zero".to_owned(),
405 ));
406 }
407 if self.create_topic && self.partitions == 0 {
408 return Err(LaserError::Invalid(
409 "topic partition count must be greater than zero".to_owned(),
410 ));
411 }
412 let background = self.background.is_some();
413 let mut publish_options = self.topic.laser.publish_options();
414 publish_options.max_retries = self.retries.unwrap_or(0);
415 if let Some(interval) = self.retry_interval {
416 publish_options.retry_backoff = interval;
417 }
418 publish_options.validate()?;
419 let mut builder = self.topic.iggy_producer()?;
420 builder = match self.background {
421 Some(config) => builder.background(config),
422 None => builder.direct(
423 DirectConfig::builder()
424 .batch_length(self.batch_length)
425 .linger_time(self.linger.into())
426 .build(),
427 ),
428 };
429 let retry_interval = self
430 .retry_interval
431 .map(|duration| {
432 positive_duration(
433 duration,
434 "producer retry interval must be greater than zero",
435 )
436 })
437 .transpose()?;
438 builder = builder
439 .partitioning(self.routing.into_partitioning()?)
440 .send_retries(
441 if background { self.retries } else { Some(0) },
442 retry_interval,
443 );
444 builder = if self.create_stream {
445 builder.create_stream_if_not_exists()
446 } else {
447 builder.do_not_create_stream_if_not_exists()
448 };
449 builder = if self.create_topic {
450 builder.create_topic_if_not_exists(self.partitions, expiry, max_topic_size)
451 } else {
452 builder.do_not_create_topic_if_not_exists()
453 };
454 let producer = builder.build();
455 let observed = std::sync::atomic::AtomicU64::new(0);
456 publish_options
457 .run(
458 || async {
459 observed.store(
460 self.topic.laser.publish_generation(),
461 std::sync::atomic::Ordering::Release,
462 );
463 Ok(crate::laser::init_producer(&producer).await?)
464 },
465 || {
466 self.topic
467 .laser
468 .reconnect_for_publish(observed.load(std::sync::atomic::Ordering::Acquire))
469 },
470 )
471 .await?;
472 let statistics = super::producer_statistics::ProducerRecorder::new(
473 &self.topic.laser,
474 producer.stream().to_string(),
475 producer.topic().to_string(),
476 !background,
477 );
478 Ok(Producer {
479 statistics,
480 inner: Arc::new(producer),
481 laser: self.topic.laser.clone(),
482 publish_options,
483 batch_length: if self.batch_length == 0 {
484 DEFAULT_BATCH_LENGTH
485 } else {
486 self.batch_length
487 } as usize,
488 background,
489 })
490 }
491}
492
493#[derive(Clone)]
494pub struct Producer {
496 statistics: super::producer_statistics::ProducerRecorder,
497 inner: Arc<IggyProducer>,
498 laser: crate::laser::Laser,
499 publish_options: crate::laser::PublishOptions,
500 batch_length: usize,
501 background: bool,
502}
503
504impl Producer {
505 pub async fn send(
507 &self,
508 payload: impl Into<Bytes>,
509 ) -> Result<SendMessagesResponse, LaserError> {
510 self.send_message(ProducerMessage::new(payload)).await
511 }
512
513 pub async fn send_message(
515 &self,
516 message: ProducerMessage,
517 ) -> Result<SendMessagesResponse, LaserError> {
518 self.send_iggy(vec![message.into_iggy()?], None).await
519 }
520
521 pub async fn send_with_routing(
523 &self,
524 message: ProducerMessage,
525 routing: Routing,
526 ) -> Result<SendMessagesResponse, LaserError> {
527 self.send_iggy(
528 vec![message.into_iggy()?],
529 Some(Arc::new(routing.into_partitioning()?)),
530 )
531 .await
532 }
533
534 pub async fn send_keyed(
536 &self,
537 message: ProducerMessage,
538 key: impl Into<Vec<u8>>,
539 ) -> Result<SendMessagesResponse, LaserError> {
540 self.send_with_routing(message, Routing::key(key)).await
541 }
542
543 pub async fn send_to_partition(
545 &self,
546 message: ProducerMessage,
547 partition: u32,
548 ) -> Result<SendMessagesResponse, LaserError> {
549 self.send_with_routing(message, Routing::Partition(partition))
550 .await
551 }
552
553 pub async fn send_batch(
555 &self,
556 messages: impl IntoIterator<Item = ProducerMessage>,
557 ) -> Result<SendMessagesResponse, LaserError> {
558 self.send_batch_with_routing(messages, None).await
559 }
560
561 pub async fn send_batch_with_routing(
563 &self,
564 messages: impl IntoIterator<Item = ProducerMessage>,
565 routing: Option<Routing>,
566 ) -> Result<SendMessagesResponse, LaserError> {
567 let messages = messages
568 .into_iter()
569 .map(ProducerMessage::into_iggy)
570 .collect::<Result<Vec<_>, _>>()?;
571 if messages.is_empty() {
572 return Ok(SendMessagesResponse {
573 confirmations: Vec::new(),
574 });
575 }
576 let routing = routing
577 .map(Routing::into_partitioning)
578 .transpose()?
579 .map(Arc::new);
580 self.send_iggy(messages, routing).await
581 }
582
583 async fn send_iggy(
584 &self,
585 messages: Vec<IggyMessage>,
586 routing: Option<Arc<Partitioning>>,
587 ) -> Result<SendMessagesResponse, LaserError> {
588 let observation = self.statistics.begin(
589 messages.len() as u64,
590 messages
591 .iter()
592 .map(|message| message.payload.len() as u64)
593 .sum(),
594 );
595 let result = self.send_iggy_observed(messages, routing).await;
596 observation.finish(result.is_ok());
597 result
598 }
599
600 async fn send_iggy_observed(
601 &self,
602 mut messages: Vec<IggyMessage>,
603 routing: Option<Arc<Partitioning>>,
604 ) -> Result<SendMessagesResponse, LaserError> {
605 if self.background {
606 return Ok(self.inner.send_with_partitioning(messages, routing).await?);
607 }
608 crate::laser::prepare_publish_messages(&mut messages);
609 let mut confirmations = Vec::new();
610 let observed = std::sync::atomic::AtomicU64::new(0);
611 for (index, chunk) in messages.chunks(self.batch_length).enumerate() {
612 let response = self
613 .publish_options
614 .run(
615 || async {
616 observed.store(
617 self.laser.publish_generation(),
618 std::sync::atomic::Ordering::Release,
619 );
620 Ok(self
621 .inner
622 .send_with_partitioning(
623 chunk.iter().map(crate::laser::clone_iggy_message).collect(),
624 routing.clone(),
625 )
626 .await?)
627 },
628 || {
629 self.laser.reconnect_for_publish(
630 observed.load(std::sync::atomic::Ordering::Acquire),
631 )
632 },
633 )
634 .await
635 .map_err(|error| {
636 crate::laser::publish_failure(
637 error,
638 &messages[index * self.batch_length..],
639 std::mem::take(&mut confirmations),
640 chunk.len(),
641 (
642 &self.inner.stream().to_string(),
643 &self.inner.topic().to_string(),
644 ),
645 )
646 })?;
647 confirmations.extend(response.confirmations);
648 }
649 Ok(SendMessagesResponse { confirmations })
650 }
651
652 pub async fn shutdown(self) -> Result<(), LaserError> {
658 match Arc::try_unwrap(self.inner) {
659 Ok(producer) => {
660 producer.shutdown().await;
661 Ok(())
662 }
663 Err(_) => Err(LaserError::Invalid(
664 "producer still has other live handles, drop them before shutdown".to_owned(),
665 )),
666 }
667 }
668}
669
670#[derive(Clone)]
671enum ConsumerTarget {
672 Partition { name: String, partition: u32 },
673 Group(Box<ConsumerGroup>),
674}
675
676#[derive(Clone)]
685pub struct ConsumerBuilder {
686 topic: Topic,
687 target: ConsumerTarget,
688 batch_length: u32,
689 poll_interval: Option<Duration>,
690 start: ConsumerStart,
691 commit: CommitPolicy,
692 auto_join_group: bool,
693 create_group: bool,
694 polling_retry_interval: Duration,
695 init_retries: Option<(u32, Duration)>,
696 allow_replay: bool,
697 native: bool,
698}
699
700impl ConsumerBuilder {
701 pub(crate) fn partition(topic: Topic, name: impl Into<String>, partition: u32) -> Self {
702 Self::new(
703 topic,
704 ConsumerTarget::Partition {
705 name: name.into(),
706 partition,
707 },
708 )
709 }
710
711 pub(crate) fn group(group: ConsumerGroup) -> Self {
712 Self::new(
713 group.topic().clone(),
714 ConsumerTarget::Group(Box::new(group)),
715 )
716 }
717
718 fn new(topic: Topic, target: ConsumerTarget) -> Self {
719 Self {
720 topic,
721 target,
722 batch_length: DEFAULT_BATCH_LENGTH,
723 poll_interval: None,
724 start: ConsumerStart::Next,
725 commit: CommitPolicy::Polling,
726 auto_join_group: true,
727 create_group: true,
728 polling_retry_interval: DEFAULT_RETRY_INTERVAL,
729 init_retries: None,
730 allow_replay: false,
731 native: false,
732 }
733 }
734
735 #[cfg_attr(not(feature = "agent"), allow(dead_code))]
739 pub(crate) fn native(mut self) -> Self {
740 self.native = true;
741 self
742 }
743
744 #[must_use]
749 pub fn batch_length(mut self, batch_length: u32) -> Self {
750 self.batch_length = batch_length;
751 self
752 }
753
754 #[must_use]
756 pub fn poll_interval(mut self, poll_interval: Duration) -> Self {
757 self.poll_interval = Some(poll_interval);
758 self
759 }
760
761 #[must_use]
764 pub fn without_poll_interval(mut self) -> Self {
765 self.poll_interval = None;
766 self
767 }
768
769 #[must_use]
770 pub fn start_at(mut self, start: ConsumerStart) -> Self {
771 self.start = start;
772 self
773 }
774
775 #[must_use]
776 pub fn commit_policy(mut self, commit: CommitPolicy) -> Self {
777 self.commit = commit;
778 self
779 }
780
781 #[must_use]
783 pub fn auto_join_group(mut self, auto_join: bool) -> Self {
784 self.auto_join_group = auto_join;
785 self
786 }
787
788 #[must_use]
789 pub fn create_group(mut self, create: bool) -> Self {
790 self.create_group = create;
791 self
792 }
793
794 #[must_use]
796 pub fn polling_retry_interval(mut self, interval: Duration) -> Self {
797 self.polling_retry_interval = interval;
798 self
799 }
800
801 #[must_use]
802 pub fn init_retries(mut self, retries: u32, interval: Duration) -> Self {
803 self.init_retries = Some((retries, interval));
804 self
805 }
806
807 #[must_use]
810 pub fn allow_replay(mut self) -> Self {
811 self.allow_replay = true;
812 self
813 }
814
815 pub async fn build(self) -> Result<Consumer, LaserError> {
816 if self.batch_length == 0 {
817 return Err(LaserError::Invalid(
818 "consumer batch length must be greater than zero".to_owned(),
819 ));
820 }
821 if let ConsumerTarget::Group(group) = &self.target
822 && !self.native
823 {
824 let mut capabilities = self.topic.laser().capabilities().await;
825 if capabilities.hello == HelloOutcome::Unknown {
826 capabilities = self.topic.laser().refresh_capabilities().await;
827 }
828 if policy_aware(&capabilities)? {
829 let group = group.as_ref().clone();
830 return self.build_group(group).await;
831 }
832 }
833 self.build_native().await
834 }
835
836 async fn build_group(self, group: ConsumerGroup) -> Result<Consumer, LaserError> {
837 self.commit.into_auto_commit()?;
838 if !self.auto_join_group {
839 return Err(LaserError::Invalid(
840 "a policy-aware group consumer always joins its group, remove auto_join_group(false)"
841 .to_owned(),
842 ));
843 }
844 if self.create_group && group.name().is_some() {
845 group.ensure_native().await?;
846 }
847 let polling_retry_interval = positive_duration(
848 self.polling_retry_interval,
849 "consumer polling retry interval must be greater than zero",
850 )?;
851 let mut retries_left = self.init_retries;
852 let reader = loop {
853 let built = group
854 .reader_with(FilterRef::Group)?
855 .start(self.start.into_filtered())
856 .count(self.batch_length)
857 .max_examined(self.batch_length)
858 .idle_interval(self.poll_interval.unwrap_or(DEFAULT_IDLE_INTERVAL))
859 .retry_interval(polling_retry_interval)
860 .build()
861 .await;
862 match built {
863 Ok(reader) => break reader,
864 Err(error)
865 if error.is_retryable()
866 && retries_left.is_some_and(|(retries, _)| retries > 0) =>
867 {
868 let (retries, interval) = retries_left.unwrap_or_default();
869 retries_left = Some((retries - 1, interval));
870 sleep(positive_duration(
871 interval,
872 "consumer initialization retry interval must be greater than zero",
873 )?)
874 .await;
875 }
876 Err(error) => return Err(error),
877 }
878 };
879 let offset_owner = OffsetOwner {
880 laser: self.topic.laser.clone(),
881 stream: self.topic.stream()?.to_owned(),
882 topic: self.topic.name.clone(),
883 consumer: group_offset_consumer(&group)?,
884 };
885 let offsets = Arc::new(std::sync::Mutex::new(GroupOffsets::default()));
886 let deferred = Arc::new(std::sync::Mutex::new(Vec::new()));
887 Ok(Consumer {
888 yielded_zero: BTreeSet::new(),
889 inner: Some(ConsumerInner::Group(Arc::new(tokio::sync::Mutex::new(
890 GroupEngine {
891 reader,
892 commit: self.commit,
893 buffered: VecDeque::new(),
894 pending_delivery: None,
895 pending_flush: None,
896 returned: VecDeque::new(),
897 yielded_since_flush: 0,
898 last_flush: Instant::now(),
899 offsets: Arc::clone(&offsets),
900 deferred: Arc::clone(&deferred),
901 },
902 )))),
903 manual_commit: self.commit == CommitPolicy::Disabled,
904 shutdown_target: None,
905 next_future: std::sync::Mutex::new(None),
906 offsets: Some(offsets),
907 deferred: Some(deferred),
908 returned_native: std::sync::Mutex::new(VecDeque::new()),
909 offset_owner,
910 })
911 }
912
913 async fn build_native(self) -> Result<Consumer, LaserError> {
914 let manual_commit = self.commit == CommitPolicy::Disabled;
915 let native_group = match &self.target {
916 ConsumerTarget::Group(group) => Some(group.native_name().await?),
917 ConsumerTarget::Partition { .. } => None,
918 };
919 let shutdown_target = match &self.target {
920 ConsumerTarget::Group(_) if manual_commit && self.auto_join_group => {
921 Some(ConsumerGroupTarget {
922 laser: self.topic.laser.clone(),
923 stream: self.topic.stream()?.to_owned(),
924 topic: self.topic.name.clone(),
925 group: native_group
926 .clone()
927 .ok_or(LaserError::Config("consumer group name is absent"))?,
928 })
929 }
930 _ => None,
931 };
932 let offset_owner = OffsetOwner {
933 laser: self.topic.laser.clone(),
934 stream: self.topic.stream()?.to_owned(),
935 topic: self.topic.name.clone(),
936 consumer: match (&self.target, &native_group) {
937 (ConsumerTarget::Partition { name, .. }, _) => {
938 iggy::prelude::Consumer::new(Identifier::named(name)?)
939 }
940 (ConsumerTarget::Group(_), Some(group)) => {
941 iggy::prelude::Consumer::group(Identifier::named(group)?)
942 }
943 (ConsumerTarget::Group(_), None) => {
944 return Err(LaserError::Config("consumer group name is absent"));
945 }
946 },
947 };
948 let group = matches!(self.target, ConsumerTarget::Group(_));
949 let mut builder: IggyConsumerBuilder = match &self.target {
950 ConsumerTarget::Partition { name, partition } => {
951 self.topic.iggy_consumer(name, *partition)?
952 }
953 ConsumerTarget::Group(_) => self.topic.iggy_consumer_group(
954 native_group
955 .as_deref()
956 .ok_or(LaserError::Config("consumer group name is absent"))?,
957 )?,
958 };
959 builder = builder
960 .batch_length(self.batch_length)
961 .polling_strategy(self.start.into_polling())
962 .auto_commit(self.commit.into_auto_commit()?)
963 .polling_retry_interval(positive_duration(
964 self.polling_retry_interval,
965 "consumer polling retry interval must be greater than zero",
966 )?);
967 builder = match self.poll_interval {
968 Some(interval) => builder.poll_interval(interval.into()),
969 None => builder.without_poll_interval(),
970 };
971 if group {
972 builder = if self.auto_join_group {
973 builder.auto_join_consumer_group()
974 } else {
975 builder.do_not_auto_join_consumer_group()
976 };
977 builder = if self.create_group {
978 builder.create_consumer_group_if_not_exists()
979 } else {
980 builder.do_not_create_consumer_group_if_not_exists()
981 };
982 }
983 if let Some((retries, interval)) = self.init_retries {
984 builder = builder.init_retries(
985 retries,
986 positive_duration(
987 interval,
988 "consumer initialization retry interval must be greater than zero",
989 )?,
990 );
991 }
992 if self.allow_replay {
993 builder = builder.allow_replay();
994 }
995 let mut consumer = builder.build();
996 consumer.init().await?;
997 Ok(Consumer {
998 yielded_zero: BTreeSet::new(),
999 inner: Some(ConsumerInner::Native(Box::new(consumer))),
1000 manual_commit,
1001 shutdown_target,
1002 next_future: std::sync::Mutex::new(None),
1003 offsets: None,
1004 deferred: None,
1005 returned_native: std::sync::Mutex::new(VecDeque::new()),
1006 offset_owner,
1007 })
1008 }
1009}
1010
1011pub(crate) fn policy_aware(capabilities: &Capabilities) -> Result<bool, LaserError> {
1019 if capabilities.filters.group_policy_reads {
1020 return Ok(true);
1021 }
1022 if capabilities.filters.native {
1023 return Err(LaserError::unsupported_feature(
1024 "filters",
1025 "group_policy_reads",
1026 "this server serves consumer filters but not group-aware reads, upgrade it before consuming groups through the Laser SDK",
1027 ));
1028 }
1029 match capabilities.hello {
1030 HelloOutcome::Unknown | HelloOutcome::Failed => Err(LaserError::Timeout(
1031 "the managed probe that decides whether this server resolves consumer group policies, reconnect and build the consumer again",
1032 )),
1033 HelloOutcome::Answered | HelloOutcome::Rejected => Ok(false),
1034 }
1035}
1036
1037fn group_offset_consumer(group: &ConsumerGroup) -> Result<iggy::prelude::Consumer, LaserError> {
1040 let id =
1041 match (group.name(), group.id()) {
1042 (Some(name), _) => Identifier::named(name)?,
1043 (None, Some(id)) => Identifier::numeric(u32::try_from(id).map_err(|_| {
1044 LaserError::Invalid("consumer group id exceeds 32 bits".to_owned())
1045 })?)?,
1046 (None, None) => return Err(LaserError::Config("consumer group name is absent")),
1047 };
1048 Ok(iggy::prelude::Consumer::group(id))
1049}
1050
1051#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1053enum CommitPoint {
1054 Delivered { last_of_page: bool },
1056 Poll,
1058}
1059
1060const fn commit_due(
1064 commit: CommitPolicy,
1065 yielded: u32,
1066 elapsed: Duration,
1067 point: CommitPoint,
1068) -> bool {
1069 let polling = matches!(point, CommitPoint::Poll) && yielded > 0;
1070 let last_of_page = matches!(point, CommitPoint::Delivered { last_of_page: true });
1071 let delivered = matches!(point, CommitPoint::Delivered { .. });
1072 match commit {
1073 CommitPolicy::Disabled => false,
1074 CommitPolicy::Polling => polling,
1075 CommitPolicy::Interval(interval) => elapsed.as_nanos() >= interval.as_nanos(),
1076 CommitPolicy::IntervalOrPolling(interval) => {
1077 polling || elapsed.as_nanos() >= interval.as_nanos()
1078 }
1079 CommitPolicy::All => last_of_page,
1080 CommitPolicy::IntervalOrAll(interval) => {
1081 last_of_page || elapsed.as_nanos() >= interval.as_nanos()
1082 }
1083 CommitPolicy::Each | CommitPolicy::IntervalOrEach(_) => delivered && yielded > 0,
1084 CommitPolicy::Every(every) => delivered && yielded >= every,
1085 CommitPolicy::IntervalOrEvery(interval, every) => {
1086 (delivered && yielded >= every) || elapsed.as_nanos() >= interval.as_nanos()
1087 }
1088 }
1089}
1090
1091#[derive(Debug, Clone)]
1092pub struct ConsumerMessage {
1094 pub payload: Bytes,
1095 pub headers: Headers,
1096 pub message_id: u128,
1097 pub checksum: u64,
1098 pub position: MessageId,
1099 pub current_offset: u64,
1100 pub partition_id: u32,
1101 pub timestamp_micros: u64,
1102 pub origin_timestamp_micros: u64,
1103 pub user_headers: Option<Bytes>,
1106 pub headers_malformed: bool,
1109 pub(crate) delivery: Option<Delivery>,
1112}
1113
1114impl ConsumerMessage {
1115 pub fn json<T: DeserializeOwned>(&self) -> Result<T, LaserError> {
1117 serde_json::from_slice(&self.payload).map_err(|error| LaserError::Codec(error.to_string()))
1118 }
1119
1120 pub(crate) fn of(
1124 message: IggyMessage,
1125 partition_id: u32,
1126 current_offset: u64,
1127 delivery: Option<Delivery>,
1128 ) -> Self {
1129 let (headers, headers_malformed) = match message.user_headers_map() {
1130 Ok(headers) => (headers.unwrap_or_default(), false),
1131 Err(_) => (Headers::new(), true),
1132 };
1133 let header = message.header;
1134 Self {
1135 payload: message.payload,
1136 headers,
1137 message_id: header.id,
1138 checksum: header.checksum,
1139 position: MessageId::new(partition_id, header.offset),
1140 current_offset,
1141 partition_id,
1142 timestamp_micros: header.timestamp,
1143 origin_timestamp_micros: header.origin_timestamp,
1144 user_headers: message.user_headers,
1145 headers_malformed,
1146 delivery,
1147 }
1148 }
1149}
1150
1151impl From<ReceivedMessage> for ConsumerMessage {
1152 fn from(received: ReceivedMessage) -> Self {
1153 Self::of(
1154 received.message,
1155 received.partition_id,
1156 received.current_offset,
1157 None,
1158 )
1159 }
1160}
1161
1162pub struct Consumer {
1167 yielded_zero: BTreeSet<u32>,
1168 inner: Option<ConsumerInner>,
1169 manual_commit: bool,
1170 shutdown_target: Option<ConsumerGroupTarget>,
1171 next_future: std::sync::Mutex<Option<NextFuture>>,
1174 offsets: Option<Arc<std::sync::Mutex<GroupOffsets>>>,
1175 #[cfg_attr(not(feature = "agent"), allow(dead_code))]
1177 deferred: Option<DeferredAcks>,
1178 returned_native: std::sync::Mutex<VecDeque<ConsumerMessage>>,
1179 offset_owner: OffsetOwner,
1180}
1181
1182type DeferredAcks = Arc<std::sync::Mutex<Vec<Delivery>>>;
1183
1184#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1187pub struct StoredOffset {
1188 pub stored_offset: u64,
1190 pub current_offset: u64,
1192}
1193
1194type NextFuture = Pin<Box<dyn Future<Output = Option<Result<ConsumerMessage, LaserError>>> + Send>>;
1195
1196enum ConsumerInner {
1197 Native(Box<IggyConsumer>),
1198 Group(Arc<tokio::sync::Mutex<GroupEngine>>),
1199}
1200
1201struct ConsumerGroupTarget {
1202 laser: crate::laser::Laser,
1203 stream: String,
1204 topic: String,
1205 group: String,
1206}
1207
1208struct OffsetOwner {
1211 laser: crate::laser::Laser,
1212 stream: String,
1213 topic: String,
1214 consumer: iggy::prelude::Consumer,
1215}
1216
1217#[derive(Default)]
1220struct GroupOffsets {
1221 consumed: BTreeMap<u32, u64>,
1222 stored: BTreeMap<u32, u64>,
1223 assigned: Option<BTreeSet<u32>>,
1225}
1226
1227struct GroupEngine {
1232 reader: FilteredReader,
1233 commit: CommitPolicy,
1234 buffered: VecDeque<MatchedRecord>,
1235 pending_delivery: Option<(Delivery, bool)>,
1236 pending_flush: Option<CommitPoint>,
1237 returned: VecDeque<(ConsumerMessage, bool)>,
1238 yielded_since_flush: u32,
1239 last_flush: Instant,
1240 offsets: Arc<std::sync::Mutex<GroupOffsets>>,
1241 deferred: DeferredAcks,
1242}
1243
1244impl GroupEngine {
1245 async fn next(&mut self) -> Option<Result<ConsumerMessage, LaserError>> {
1246 if let Err(error) = self.finish_delivery().await {
1247 return Some(Err(error));
1248 }
1249 if let Err(error) = self.apply_deferred().await {
1250 return Some(Err(error));
1251 }
1252 loop {
1253 if let Some((message, last_of_page)) = self.returned.pop_front() {
1254 if let Some(delivery) = &message.delivery {
1255 self.pending_delivery = Some((delivery.clone(), last_of_page));
1256 }
1257 return Some(Ok(message));
1258 }
1259 if let Some(record) = self.buffered.pop_front() {
1260 let delivery = record.delivery();
1261 self.pending_delivery = Some((delivery.clone(), self.buffered.is_empty()));
1262 self.offsets
1263 .lock()
1264 .unwrap_or_else(std::sync::PoisonError::into_inner)
1265 .consumed
1266 .insert(delivery.partition_id, delivery.offset);
1267 return Some(Ok(ConsumerMessage::of(
1268 record.message,
1269 record.partition_id,
1270 record.frontier,
1271 Some(delivery),
1272 )));
1273 }
1274 let page = loop {
1278 if let Err(error) = self.apply_deferred().await {
1279 return Some(Err(error));
1280 }
1281 if let Err(error) = self.flush_if_due(CommitPoint::Poll).await {
1282 return Some(Err(error));
1283 }
1284 match self.reader.read_round().await {
1285 Ok((Some(page), _)) => break page,
1286 Ok((None, true)) => {}
1287 Ok((None, false)) => sleep(self.reader.idle_interval()).await,
1288 Err(error) => {
1289 self.sync_stored();
1290 return Some(Err(error));
1291 }
1292 }
1293 };
1294 self.sync_stored();
1295 self.buffered.extend(page.records);
1296 }
1297 }
1298
1299 async fn finish_delivery(&mut self) -> Result<(), LaserError> {
1300 if let Some((delivery, last_of_page)) = self.pending_delivery.take()
1301 && self.commit != CommitPolicy::Disabled
1302 {
1303 self.reader.handled(&delivery);
1304 self.yielded_since_flush = self.yielded_since_flush.saturating_add(1);
1305 self.pending_flush = Some(CommitPoint::Delivered { last_of_page });
1306 }
1307 if let Some(point) = self.pending_flush {
1308 self.flush_if_due(point).await?;
1309 self.pending_flush = None;
1310 }
1311 Ok(())
1312 }
1313
1314 async fn flush_if_due(&mut self, point: CommitPoint) -> Result<(), LaserError> {
1315 if !commit_due(
1316 self.commit,
1317 self.yielded_since_flush,
1318 self.last_flush.elapsed(),
1319 point,
1320 ) {
1321 return Ok(());
1322 }
1323 if let Err(error) = self.reader.flush_completed().await {
1324 let reason = error.filter_reason();
1325 if matches!(
1326 reason,
1327 Some(
1328 laser_wire::filter::FilterErrorReason::Conflict
1329 | laser_wire::filter::FilterErrorReason::SourceChanged
1330 )
1331 ) {
1332 self.buffered
1333 .retain(|record| self.reader.owns_record(record));
1334 self.pending_flush = None;
1335 self.yielded_since_flush = 0;
1336 }
1337 self.sync_stored();
1338 if reason == Some(laser_wire::filter::FilterErrorReason::Conflict) {
1342 tracing::warn!(target: "laser", %error, "the group's policy changed, progress read under the old policy is delivered again");
1343 self.last_flush = Instant::now();
1344 return Ok(());
1345 }
1346 return Err(error);
1347 }
1348 self.yielded_since_flush = 0;
1349 self.last_flush = Instant::now();
1350 self.sync_stored();
1351 Ok(())
1352 }
1353
1354 async fn commit(&mut self, delivery: &Delivery) -> Result<(), LaserError> {
1355 self.reader.ack_through_delivery(delivery).await?;
1356 self.sync_stored();
1357 Ok(())
1358 }
1359
1360 async fn apply_deferred(&mut self) -> Result<(), LaserError> {
1366 loop {
1367 let next = self
1368 .deferred
1369 .lock()
1370 .unwrap_or_else(std::sync::PoisonError::into_inner)
1371 .first()
1372 .cloned();
1373 let Some(delivery) = next else {
1374 return Ok(());
1375 };
1376 match self.commit(&delivery).await {
1377 Ok(()) => {}
1378 Err(error)
1379 if error.filter_reason()
1380 == Some(laser_wire::filter::FilterErrorReason::MembershipStale) =>
1381 {
1382 tracing::debug!(target: "laser", %error, partition_id = delivery.partition_id, "a handled record of a lost partition is read again by its new owner");
1383 }
1384 Err(error) => return Err(error),
1385 }
1386 self.deferred
1387 .lock()
1388 .unwrap_or_else(std::sync::PoisonError::into_inner)
1389 .remove(0);
1390 }
1391 }
1392
1393 fn sync_stored(&self) {
1394 let mut offsets = self
1395 .offsets
1396 .lock()
1397 .unwrap_or_else(std::sync::PoisonError::into_inner);
1398 offsets.stored = self.reader.stored_offsets();
1399 offsets.assigned = Some(self.reader.partitions().into_iter().collect());
1400 }
1401}
1402
1403impl Consumer {
1404 pub async fn next(&mut self) -> Option<Result<ConsumerMessage, LaserError>> {
1406 futures::StreamExt::next(self).await
1407 }
1408
1409 #[doc(hidden)]
1411 pub async fn return_delivery(&self, message: ConsumerMessage) -> Result<(), LaserError> {
1412 match self.inner.as_ref() {
1413 Some(ConsumerInner::Group(engine)) => {
1414 *self
1415 .next_future
1416 .lock()
1417 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1418 let mut engine = engine.lock().await;
1419 let pending = engine.pending_delivery.as_ref().ok_or_else(|| {
1420 LaserError::Invalid(
1421 "only the latest unhandled delivery can be returned".to_owned(),
1422 )
1423 })?;
1424 if !message
1425 .delivery
1426 .as_ref()
1427 .is_some_and(|delivery| pending.0.same(delivery))
1428 {
1429 return Err(LaserError::Invalid(
1430 "the delivery is not the latest one from this consumer".to_owned(),
1431 ));
1432 }
1433 let last_of_page = pending.1;
1434 engine.pending_delivery = None;
1435 engine.returned.push_front((message, last_of_page));
1436 Ok(())
1437 }
1438 Some(ConsumerInner::Native(_)) => {
1439 self.returned_native
1440 .lock()
1441 .unwrap_or_else(std::sync::PoisonError::into_inner)
1442 .push_front(message);
1443 Ok(())
1444 }
1445 None => Err(LaserError::Invalid(
1446 "consumer has been shut down".to_owned(),
1447 )),
1448 }
1449 }
1450
1451 pub async fn next_within(&mut self, wait: Duration) -> Result<ConsumerMessage, LaserError> {
1465 tokio::time::timeout(wait, self.next())
1466 .await
1467 .map_err(|_| LaserError::Timeout("the live consumer to yield a record"))?
1468 .ok_or_else(|| LaserError::Invalid("the live consumer stream ended".to_owned()))?
1469 }
1470
1471 pub async fn commit(&self, message: &ConsumerMessage) -> Result<(), LaserError> {
1475 match self.inner.as_ref() {
1476 Some(ConsumerInner::Group(engine)) => {
1477 let delivery = message.delivery.as_ref().ok_or_else(|| {
1478 LaserError::Invalid(
1479 "the record was not delivered by this group consumer".to_owned(),
1480 )
1481 })?;
1482 *self
1483 .next_future
1484 .lock()
1485 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1486 engine.lock().await.commit(delivery).await
1487 }
1488 _ => {
1489 self.store_offset(message.position.offset, Some(message.partition_id))
1490 .await
1491 }
1492 }
1493 }
1494
1495 #[cfg_attr(not(feature = "agent"), allow(dead_code))]
1500 pub(crate) async fn commit_handled(&self, message: &ConsumerMessage) -> Result<(), LaserError> {
1501 match (self.inner.as_ref(), &self.deferred) {
1502 (Some(ConsumerInner::Group(_)), Some(deferred)) => {
1503 let delivery = message.delivery.clone().ok_or_else(|| {
1504 LaserError::Invalid(
1505 "the record was not delivered by this group consumer".to_owned(),
1506 )
1507 })?;
1508 deferred
1509 .lock()
1510 .unwrap_or_else(std::sync::PoisonError::into_inner)
1511 .push(delivery);
1512 Ok(())
1513 }
1514 _ => self.commit(message).await,
1515 }
1516 }
1517
1518 #[cfg_attr(not(feature = "agent"), allow(dead_code))]
1522 pub(crate) fn assigned_partitions(&self) -> Option<BTreeSet<u32>> {
1523 self.offsets.as_ref().and_then(|offsets| {
1524 offsets
1525 .lock()
1526 .unwrap_or_else(std::sync::PoisonError::into_inner)
1527 .assigned
1528 .clone()
1529 })
1530 }
1531
1532 pub async fn store_offset(
1536 &self,
1537 offset: u64,
1538 partition: Option<u32>,
1539 ) -> Result<(), LaserError> {
1540 self.native()?.store_offset(offset, partition).await?;
1541 Ok(())
1542 }
1543
1544 pub async fn delete_offset(&self, partition: Option<u32>) -> Result<(), LaserError> {
1547 self.native()?.delete_offset(partition).await?;
1548 Ok(())
1549 }
1550
1551 pub fn last_consumed_offset(&self, partition: u32) -> Option<u64> {
1553 match self.inner.as_ref() {
1554 Some(ConsumerInner::Native(consumer)) => consumer.get_last_consumed_offset(partition),
1555 Some(ConsumerInner::Group(_)) => self.offsets.as_ref().and_then(|offsets| {
1556 offsets
1557 .lock()
1558 .unwrap_or_else(std::sync::PoisonError::into_inner)
1559 .consumed
1560 .get(&partition)
1561 .copied()
1562 }),
1563 None => None,
1564 }
1565 }
1566
1567 pub fn last_stored_offset(&self, partition: u32) -> Option<u64> {
1572 match self.inner.as_ref() {
1573 Some(ConsumerInner::Native(consumer)) => consumer.get_last_stored_offset(partition),
1574 Some(ConsumerInner::Group(_)) => self.offsets.as_ref().and_then(|offsets| {
1575 offsets
1576 .lock()
1577 .unwrap_or_else(std::sync::PoisonError::into_inner)
1578 .stored
1579 .get(&partition)
1580 .copied()
1581 }),
1582 None => None,
1583 }
1584 }
1585
1586 pub async fn stored_offset(&self, partition: u32) -> Result<Option<StoredOffset>, LaserError> {
1591 let owner = &self.offset_owner;
1592 let info = owner
1593 .laser
1594 .client()
1595 .get_consumer_offset(
1596 &owner.consumer,
1597 &Identifier::named(&owner.stream)?,
1598 &Identifier::named(&owner.topic)?,
1599 Some(partition),
1600 )
1601 .await?;
1602 Ok(info.map(|info| StoredOffset {
1603 stored_offset: info.stored_offset,
1604 current_offset: info.current_offset,
1605 }))
1606 }
1607
1608 pub async fn shutdown(&mut self) -> Result<(), LaserError> {
1614 *self
1615 .next_future
1616 .get_mut()
1617 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1618 match self.inner.take() {
1619 None => Ok(()),
1620 Some(ConsumerInner::Group(engine)) => {
1621 self.shutdown_target.take();
1622 let engine = Arc::try_unwrap(engine).map_err(|_| {
1623 LaserError::Invalid("the consumer is still being polled".to_owned())
1624 })?;
1625 let mut engine = engine.into_inner();
1626 let completed = engine.finish_delivery().await;
1627 let completed = match completed {
1628 Ok(()) => engine.apply_deferred().await,
1629 Err(error) => Err(error),
1630 };
1631 let closed = engine.reader.close().await;
1632 completed.and(closed)
1633 }
1634 Some(ConsumerInner::Native(mut consumer)) => {
1635 if self.manual_commit {
1636 drop(consumer);
1637 if let Some(target) = self.shutdown_target.take() {
1638 target
1639 .laser
1640 .client()
1641 .leave_consumer_group(
1642 &Identifier::try_from(target.stream)?,
1643 &Identifier::try_from(target.topic)?,
1644 &Identifier::try_from(target.group)?,
1645 )
1646 .await?;
1647 }
1648 return Ok(());
1649 }
1650 let mut stored = Ok(());
1651 for partition in &self.yielded_zero {
1652 if consumer
1653 .get_last_stored_offset(*partition)
1654 .is_none_or(|offset| offset == 0)
1655 {
1656 stored = stored.and(consumer.store_offset(0, Some(*partition)).await);
1659 }
1660 }
1661 let stopped = consumer.shutdown().await;
1662 self.shutdown_target.take();
1663 stored.and(stopped).map_err(Into::into)
1664 }
1665 }
1666 }
1667
1668 fn native(&self) -> Result<&IggyConsumer, LaserError> {
1669 match self.inner.as_ref() {
1670 Some(ConsumerInner::Native(consumer)) => Ok(consumer),
1671 Some(ConsumerInner::Group(_)) => Err(LaserError::Invalid(
1672 "explicit offsets bypass the group's acknowledgment contract, commit a delivered record instead"
1673 .to_owned(),
1674 )),
1675 None => Err(LaserError::Invalid(
1676 "consumer has been shut down".to_owned(),
1677 )),
1678 }
1679 }
1680}
1681
1682impl Stream for Consumer {
1683 type Item = Result<ConsumerMessage, LaserError>;
1684
1685 fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1686 if matches!(self.inner, Some(ConsumerInner::Native(_)))
1687 && let Some(message) = self
1688 .returned_native
1689 .get_mut()
1690 .unwrap_or_else(std::sync::PoisonError::into_inner)
1691 .pop_front()
1692 {
1693 return Poll::Ready(Some(Ok(message)));
1694 }
1695 let engine = match self.inner.as_mut() {
1696 None => return Poll::Ready(None),
1697 Some(ConsumerInner::Native(inner)) => {
1698 return match Pin::new(inner).poll_next(context) {
1699 Poll::Ready(Some(Ok(message))) => {
1700 let message = ConsumerMessage::from(message);
1701 if message.position.offset == 0 {
1702 self.yielded_zero.insert(message.partition_id);
1703 } else {
1704 self.yielded_zero.remove(&message.partition_id);
1705 }
1706 Poll::Ready(Some(Ok(message)))
1707 }
1708 Poll::Ready(Some(Err(error))) => Poll::Ready(Some(Err(error.into()))),
1709 Poll::Ready(None) => Poll::Ready(None),
1710 Poll::Pending => Poll::Pending,
1711 };
1712 }
1713 Some(ConsumerInner::Group(engine)) => Arc::clone(engine),
1714 };
1715 let in_flight = self
1716 .next_future
1717 .get_mut()
1718 .unwrap_or_else(std::sync::PoisonError::into_inner);
1719 let next = in_flight
1720 .get_or_insert_with(|| Box::pin(async move { engine.lock().await.next().await }));
1721 match next.as_mut().poll(context) {
1722 Poll::Ready(item) => {
1723 *in_flight = None;
1724 Poll::Ready(item)
1725 }
1726 Poll::Pending => Poll::Pending,
1727 }
1728 }
1729}
1730
1731#[cfg(test)]
1732mod tests {
1733 use super::*;
1734
1735 fn producer_builder() -> ProducerBuilder {
1736 let laser = crate::laser::Laser::from_client(iggy::prelude::IggyClient::default());
1737 laser.stream("fleet").topic("readings").producer()
1738 }
1739
1740 #[tokio::test]
1741 async fn given_both_expiry_settings_when_building_a_producer_then_should_refuse_it() {
1742 let result = producer_builder()
1743 .expire_after(Duration::from_secs(60))
1744 .never_expire()
1745 .build()
1746 .await;
1747 assert!(
1748 matches!(result, Err(LaserError::Invalid(message)) if message.contains("not both"))
1749 );
1750 let result = producer_builder()
1751 .never_expire()
1752 .expire_after(Duration::from_secs(60))
1753 .build()
1754 .await;
1755 assert!(
1756 matches!(result, Err(LaserError::Invalid(message)) if message.contains("not both"))
1757 );
1758 }
1759
1760 #[tokio::test]
1761 async fn given_both_size_settings_when_building_a_producer_then_should_refuse_it() {
1762 let result = producer_builder()
1763 .max_topic_bytes(1_048_576)
1764 .unlimited_topic_size()
1765 .build()
1766 .await;
1767 assert!(
1768 matches!(result, Err(LaserError::Invalid(message)) if message.contains("not both"))
1769 );
1770 }
1771
1772 #[tokio::test]
1773 async fn given_a_zero_expiry_or_size_when_building_a_producer_then_should_refuse_it() {
1774 let result = producer_builder()
1775 .expire_after(Duration::ZERO)
1776 .build()
1777 .await;
1778 assert!(matches!(result, Err(LaserError::Invalid(_))));
1779 let result = producer_builder().max_topic_bytes(0).build().await;
1780 assert!(matches!(result, Err(LaserError::Invalid(_))));
1781 }
1782
1783 #[test]
1784 fn given_one_form_of_each_setting_when_resolved_then_should_map_to_the_topic_limits() {
1785 let (expiry, size) = producer_builder()
1786 .never_expire()
1787 .unlimited_topic_size()
1788 .topic_limits()
1789 .expect("one form each");
1790 assert_eq!(expiry, IggyExpiry::NeverExpire);
1791 assert_eq!(size, MaxTopicSize::Unlimited);
1792 let (expiry, size) = producer_builder().topic_limits().expect("defaults");
1793 assert_eq!(expiry, IggyExpiry::ServerDefault);
1794 assert_eq!(size, MaxTopicSize::ServerDefault);
1795 }
1796
1797 #[test]
1798 fn given_a_consumer_when_shared_across_tasks_then_should_be_send_and_sync() {
1799 fn shared<T: Send + Sync>() {}
1800 shared::<Consumer>();
1801 shared::<ConsumerBuilder>();
1802 shared::<ConsumerMessage>();
1803 }
1804
1805 #[test]
1806 fn given_a_group_handle_when_its_stored_offset_is_addressed_then_should_name_the_group() {
1807 let laser = crate::laser::Laser::from_client(iggy::prelude::IggyClient::default());
1808 let topic = laser.stream("fleet").topic("readings");
1809 let named = group_offset_consumer(&topic.consumer_group("workers")).expect("named group");
1810 assert_eq!(named.kind, iggy::prelude::ConsumerKind::ConsumerGroup);
1811 assert_eq!(named.id, Identifier::named("workers").expect("valid name"));
1812 let numeric = group_offset_consumer(&topic.consumer_group_id(7)).expect("numeric group");
1813 assert_eq!(numeric.id, Identifier::numeric(7).expect("valid id"));
1814 assert!(matches!(
1815 group_offset_consumer(&topic.consumer_group_id(u64::from(u32::MAX) + 1)),
1816 Err(LaserError::Invalid(_))
1817 ));
1818 }
1819
1820 #[test]
1821 fn given_a_server_with_group_reads_when_classified_then_should_use_the_group_engine() {
1822 let capabilities = Capabilities::OPEN.with_filters(true, true);
1823 assert!(policy_aware(&capabilities).expect("classified"));
1824 }
1825
1826 #[test]
1827 fn given_group_reads_without_an_evaluator_when_classified_then_should_preserve_group_policy() {
1828 let mut capabilities = Capabilities::OPEN;
1829 capabilities.filters.group_policy_reads = true;
1830 assert!(policy_aware(&capabilities).expect("group policy resolves without evaluation"));
1831 }
1832
1833 #[test]
1834 fn given_a_server_serving_filters_without_group_reads_when_classified_then_should_require_an_upgrade()
1835 {
1836 let mut capabilities = Capabilities::OPEN.with_filters(true, true);
1837 capabilities.filters.group_policy_reads = false;
1838 let error = policy_aware(&capabilities).expect_err("an old server is refused");
1839 assert!(matches!(
1840 error,
1841 LaserError::Unsupported {
1842 feature: Some("group_policy_reads"),
1843 ..
1844 }
1845 ));
1846 }
1847
1848 #[test]
1849 fn given_a_probe_that_established_nothing_when_classified_then_should_not_read_natively() {
1850 let mut capabilities = Capabilities::OPEN;
1851 for hello in [HelloOutcome::Unknown, HelloOutcome::Failed] {
1852 capabilities.hello = hello;
1853 assert!(matches!(
1854 policy_aware(&capabilities),
1855 Err(LaserError::Timeout(_))
1856 ));
1857 }
1858 for hello in [HelloOutcome::Rejected, HelloOutcome::Answered] {
1859 capabilities.hello = hello;
1860 assert!(
1861 !policy_aware(&capabilities).expect("positively absent"),
1862 "{hello:?} without filters reads natively"
1863 );
1864 }
1865 }
1866
1867 #[test]
1868 fn given_each_commit_policy_when_a_record_is_yielded_then_should_store_on_its_own_cadence() {
1869 let second = Duration::from_secs(1);
1870 let middle = CommitPoint::Delivered {
1871 last_of_page: false,
1872 };
1873 let last = CommitPoint::Delivered { last_of_page: true };
1874 assert!(!commit_due(CommitPolicy::Disabled, 5, second, last));
1875 assert!(!commit_due(
1876 CommitPolicy::Disabled,
1877 5,
1878 second,
1879 CommitPoint::Poll
1880 ));
1881 assert!(commit_due(CommitPolicy::Each, 1, Duration::ZERO, middle));
1882 assert!(!commit_due(
1883 CommitPolicy::Each,
1884 1,
1885 Duration::ZERO,
1886 CommitPoint::Poll
1887 ));
1888 assert!(commit_due(CommitPolicy::All, 1, Duration::ZERO, last));
1889 assert!(!commit_due(CommitPolicy::All, 1, Duration::ZERO, middle));
1890 assert!(commit_due(
1891 CommitPolicy::Every(3),
1892 3,
1893 Duration::ZERO,
1894 middle
1895 ));
1896 assert!(!commit_due(
1897 CommitPolicy::Every(3),
1898 2,
1899 Duration::ZERO,
1900 middle
1901 ));
1902 assert!(commit_due(
1903 CommitPolicy::Interval(second),
1904 0,
1905 second,
1906 CommitPoint::Poll
1907 ));
1908 assert!(!commit_due(
1909 CommitPolicy::Interval(second),
1910 9,
1911 Duration::from_millis(999),
1912 last
1913 ));
1914 assert!(commit_due(
1915 CommitPolicy::IntervalOrEvery(second, 10),
1916 10,
1917 Duration::ZERO,
1918 middle
1919 ));
1920 assert!(commit_due(
1921 CommitPolicy::IntervalOrAll(second),
1922 1,
1923 Duration::ZERO,
1924 last
1925 ));
1926 }
1927
1928 #[test]
1929 fn given_the_polling_policy_when_the_next_poll_starts_then_should_store_the_delivered_prefix() {
1930 let last = CommitPoint::Delivered { last_of_page: true };
1931 assert!(
1932 !commit_due(CommitPolicy::Polling, 4, Duration::ZERO, last),
1933 "a delivered record alone stores nothing"
1934 );
1935 assert!(commit_due(
1936 CommitPolicy::Polling,
1937 4,
1938 Duration::ZERO,
1939 CommitPoint::Poll
1940 ));
1941 assert!(
1942 !commit_due(CommitPolicy::Polling, 0, Duration::ZERO, CommitPoint::Poll),
1943 "an idle poll has nothing to store"
1944 );
1945 assert!(commit_due(
1946 CommitPolicy::IntervalOrPolling(Duration::from_secs(1)),
1947 0,
1948 Duration::from_secs(1),
1949 last
1950 ));
1951 }
1952
1953 #[test]
1954 fn given_every_start_when_mapped_to_a_group_read_then_should_keep_its_meaning() {
1955 assert_eq!(ConsumerStart::First.into_filtered(), FilteredStart::First);
1956 assert_eq!(ConsumerStart::Last.into_filtered(), FilteredStart::Last);
1957 assert_eq!(ConsumerStart::Next.into_filtered(), FilteredStart::Next);
1958 assert_eq!(
1959 ConsumerStart::Offset(7).into_filtered(),
1960 FilteredStart::Offset(7)
1961 );
1962 assert_eq!(
1963 ConsumerStart::TimestampMicros(9).into_filtered(),
1964 FilteredStart::Timestamp(9)
1965 );
1966 }
1967
1968 #[test]
1969 fn given_zero_frequency_when_building_commit_policy_then_should_reject_it() {
1970 let error = CommitPolicy::Every(0)
1971 .into_auto_commit()
1972 .expect_err("zero cannot be a commit frequency");
1973 assert!(matches!(error, LaserError::Invalid(_)));
1974 }
1975
1976 #[test]
1977 fn given_zero_interval_when_building_commit_policy_then_should_reject_it() {
1978 let error = CommitPolicy::Interval(Duration::ZERO)
1979 .into_auto_commit()
1980 .expect_err("zero cannot be a commit interval");
1981 assert!(matches!(error, LaserError::Invalid(_)));
1982 }
1983
1984 #[test]
1985 fn given_empty_key_when_building_routing_then_should_reject_it() {
1986 let error = Routing::key(Vec::new())
1987 .into_partitioning()
1988 .expect_err("an empty partition key should be rejected");
1989 assert!(matches!(error, LaserError::Iggy(_)));
1990 }
1991
1992 #[test]
1993 fn given_typed_header_when_building_message_then_should_preserve_width() {
1994 let key = HeaderKey::try_from("type").expect("the header key should be valid");
1995 let message = ProducerMessage::new(b"event".to_vec())
1996 .header(key.clone(), HeaderValue::from(7_u16))
1997 .into_iggy()
1998 .expect("the producer message should encode");
1999 let headers = message
2000 .user_headers_map()
2001 .expect("the headers should decode")
2002 .expect("the message should carry headers");
2003 assert_eq!(
2004 headers
2005 .get(&key)
2006 .expect("the type header should exist")
2007 .as_uint16()
2008 .expect("the type header should remain uint16"),
2009 7
2010 );
2011 }
2012
2013 #[test]
2014 fn given_bytes_payload_when_lowered_to_iggy_then_should_preserve_pointer_identity() {
2015 let payload = Bytes::from_static(&[0x42; 4096]);
2016 let message = ProducerMessage::new(payload.clone())
2017 .into_iggy()
2018 .expect("the producer message should lower to Iggy");
2019
2020 assert_eq!(message.payload.as_ptr(), payload.as_ptr());
2021 }
2022}