Skip to content

Commit ba92ea6

Browse files
feat(sdk): add Consumer Type extension (#1516)
1 parent 0784496 commit ba92ea6

9 files changed

+429
-118
lines changed

Cargo.lock

+13-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dependencies.md

+98-97
Large diffs are not rendered by default.

sdk/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "iggy"
3-
version = "0.6.102"
3+
version = "0.6.110"
44
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
55
edition = "2021"
66
license = "Apache-2.0"
@@ -59,6 +59,7 @@ tokio = { version = "1.43.0", features = ["full"] }
5959
tokio-rustls = { version = "0.26.1" }
6060
toml = "0.8.20"
6161
tracing = { version = "0.1.41" }
62+
trait-variant = { version = "0.1.2" }
6263
uuid = { version = "1.13.1", features = ["v7", "fast-rng", "zerocopy"] }
6364
webpki-roots = { version = "0.26.8" }
6465

sdk/src/clients/consumer.rs

+119-12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64};
2121
use std::sync::Arc;
2222
use std::task::{Context, Poll};
2323
use std::time::Duration;
24+
use tokio::time;
2425
use tokio::time::sleep;
2526
use tracing::{error, info, trace, warn};
2627

@@ -36,10 +37,18 @@ pub enum AutoCommit {
3637
Disabled,
3738
/// The auto-commit is enabled and the offset is stored on the server after a certain interval.
3839
Interval(IggyDuration),
39-
/// The auto-commit is enabled and the offset is stored on the server after a certain interval or depending on the mode.
40+
/// The auto-commit is enabled and the offset is stored on the server after a certain interval or depending on the mode when consuming the messages.
4041
IntervalOrWhen(IggyDuration, AutoCommitWhen),
41-
/// The auto-commit is enabled and the offset is stored on the server depending on the mode.
42+
/// The auto-commit is enabled and the offset is stored on the server after a certain interval or depending on the mode after consuming the messages.
43+
///
44+
/// **This will only work with the `IggyConsumerMessageExt` trait when using `consume_messages()`.**
45+
IntervalOrAfter(IggyDuration, AutoCommitAfter),
46+
/// The auto-commit is enabled and the offset is stored on the server depending on the mode when consuming the messages.
4247
When(AutoCommitWhen),
48+
/// The auto-commit is enabled and the offset is stored on the server depending on the mode after consuming the messages.
49+
///
50+
/// **This will only work with the `IggyConsumerMessageExt` trait when using `consume_messages()`.**
51+
After(AutoCommitAfter),
4352
}
4453

4554
/// The auto-commit mode for storing the offset on the server.
@@ -55,6 +64,19 @@ pub enum AutoCommitWhen {
5564
ConsumingEveryNthMessage(u32),
5665
}
5766

67+
/// The auto-commit mode for storing the offset on the server **after** receiving the messages.
68+
///
69+
/// **This will only work with the `IggyConsumerMessageExt` trait when using `consume_messages()`.**
70+
#[derive(Debug, PartialEq, Copy, Clone)]
71+
pub enum AutoCommitAfter {
72+
/// The offset is stored on the server after all the messages are consumed.
73+
ConsumingAllMessages,
74+
/// The offset is stored on the server after consuming each message.
75+
ConsumingEachMessage,
76+
/// The offset is stored on the server after consuming every Nth message.
77+
ConsumingEveryNthMessage(u32),
78+
}
79+
5880
unsafe impl Send for IggyConsumer {}
5981
unsafe impl Sync for IggyConsumer {}
6082

@@ -89,6 +111,8 @@ pub struct IggyConsumer {
89111
last_polled_at: Arc<AtomicU64>,
90112
current_partition_id: Arc<AtomicU32>,
91113
reconnection_retry_interval: IggyDuration,
114+
init_retries: Option<u32>,
115+
init_retry_interval: IggyDuration,
92116
allow_replay: bool,
93117
}
94118

@@ -108,7 +132,9 @@ impl IggyConsumer {
108132
auto_join_consumer_group: bool,
109133
create_consumer_group_if_not_exists: bool,
110134
encryptor: Option<Arc<EncryptorKind>>,
111-
retry_interval: IggyDuration,
135+
reconnection_retry_interval: IggyDuration,
136+
init_retries: Option<u32>,
137+
init_retry_interval: IggyDuration,
112138
allow_replay: bool,
113139
) -> Self {
114140
let (store_offset_sender, _) = flume::unbounded();
@@ -160,11 +186,17 @@ impl IggyConsumer {
160186
},
161187
last_polled_at: Arc::new(AtomicU64::new(0)),
162188
current_partition_id: Arc::new(AtomicU32::new(0)),
163-
reconnection_retry_interval: retry_interval,
189+
reconnection_retry_interval,
190+
init_retries,
191+
init_retry_interval,
164192
allow_replay,
165193
}
166194
}
167195

196+
pub(crate) fn auto_commit(&self) -> AutoCommit {
197+
self.auto_commit
198+
}
199+
168200
/// Returns the name of the consumer.
169201
pub fn name(&self) -> &str {
170202
&self.consumer_name
@@ -230,19 +262,65 @@ impl IggyConsumer {
230262
return Ok(());
231263
}
232264

265+
let stream_id = self.stream_id.clone();
266+
let topic_id = self.topic_id.clone();
267+
let consumer_name = &self.consumer_name;
268+
269+
info!(
270+
"Initializing consumer: {consumer_name} for stream: {stream_id}, topic: {topic_id}..."
271+
);
272+
233273
{
274+
let mut retries = 0;
275+
let init_retries = self.init_retries.unwrap_or_default();
276+
let interval = self.init_retry_interval;
277+
278+
let mut timer = time::interval(interval.get_duration());
279+
timer.tick().await;
280+
234281
let client = self.client.read().await;
235-
if client.get_stream(&self.stream_id).await?.is_none() {
282+
let mut stream_exists = client.get_stream(&stream_id).await?.is_some();
283+
let mut topic_exists = client.get_topic(&stream_id, &topic_id).await?.is_some();
284+
285+
loop {
286+
if stream_exists && topic_exists {
287+
info!("Stream: {stream_id} and topic: {topic_id} were found. Initializing consumer...",);
288+
break;
289+
}
290+
291+
if retries >= init_retries {
292+
break;
293+
}
294+
295+
retries += 1;
296+
if !stream_exists {
297+
warn!("Stream: {stream_id} does not exist. Retrying ({retries}/{init_retries}) in {interval}...",);
298+
timer.tick().await;
299+
stream_exists = client.get_stream(&stream_id).await?.is_some();
300+
}
301+
302+
if !stream_exists {
303+
continue;
304+
}
305+
306+
topic_exists = client.get_topic(&stream_id, &topic_id).await?.is_some();
307+
if topic_exists {
308+
break;
309+
}
310+
311+
warn!("Topic: {topic_id} does not exist in stream: {stream_id}. Retrying ({retries}/{init_retries}) in {interval}...",);
312+
timer.tick().await;
313+
}
314+
315+
if !stream_exists {
316+
error!("Stream: {stream_id} was not found.");
236317
return Err(IggyError::StreamNameNotFound(
237318
self.stream_id.get_string_value().unwrap_or_default(),
238319
));
239-
}
320+
};
240321

241-
if client
242-
.get_topic(&self.stream_id, &self.topic_id)
243-
.await?
244-
.is_none()
245-
{
322+
if !topic_exists {
323+
error!("Topic: {topic_id} was not found in stream: {stream_id}.");
246324
return Err(IggyError::TopicNameNotFound(
247325
self.topic_id.get_string_value().unwrap_or_default(),
248326
self.stream_id.get_string_value().unwrap_or_default(),
@@ -256,6 +334,7 @@ impl IggyConsumer {
256334
match self.auto_commit {
257335
AutoCommit::Interval(interval) => self.store_offsets_in_background(interval),
258336
AutoCommit::IntervalOrWhen(interval, _) => self.store_offsets_in_background(interval),
337+
AutoCommit::IntervalOrAfter(interval, _) => self.store_offsets_in_background(interval),
259338
_ => {}
260339
}
261340

@@ -285,6 +364,10 @@ impl IggyConsumer {
285364
});
286365

287366
self.initialized = true;
367+
info!(
368+
"Consumer: {consumer_name} has been initialized for stream: {}, topic: {}.",
369+
self.stream_id, self.topic_id
370+
);
288371
Ok(())
289372
}
290373

@@ -359,7 +442,7 @@ impl IggyConsumer {
359442
});
360443
}
361444

362-
fn send_store_offset(&self, partition_id: u32, offset: u64) {
445+
pub(crate) fn send_store_offset(&self, partition_id: u32, offset: u64) {
363446
if let Err(error) = self.store_offset_sender.send((partition_id, offset)) {
364447
error!("Failed to send offset to store: {error}, please verify if `init()` on IggyConsumer object has been called.");
365448
}
@@ -868,6 +951,8 @@ pub struct IggyConsumerBuilder {
868951
create_consumer_group_if_not_exists: bool,
869952
encryptor: Option<Arc<EncryptorKind>>,
870953
reconnection_retry_interval: IggyDuration,
954+
init_retries: Option<u32>,
955+
init_retry_interval: IggyDuration,
871956
allow_replay: bool,
872957
}
873958

@@ -901,6 +986,8 @@ impl IggyConsumerBuilder {
901986
encryptor,
902987
polling_interval,
903988
reconnection_retry_interval: IggyDuration::ONE_SECOND,
989+
init_retries: None,
990+
init_retry_interval: IggyDuration::ONE_SECOND,
904991
allow_replay: false,
905992
}
906993
}
@@ -941,6 +1028,13 @@ impl IggyConsumerBuilder {
9411028
}
9421029
}
9431030

1031+
pub fn commit_failed_messages(self) -> Self {
1032+
Self {
1033+
auto_commit: AutoCommit::Disabled,
1034+
..self
1035+
}
1036+
}
1037+
9441038
/// Automatically joins the consumer group if the consumer is a part of a consumer group.
9451039
pub fn auto_join_consumer_group(self) -> Self {
9461040
Self {
@@ -1013,6 +1107,17 @@ impl IggyConsumerBuilder {
10131107
}
10141108
}
10151109

1110+
/// Sets the number of retries and the interval when initializing the consumer if the stream or topic is not found.
1111+
/// Might be useful when the stream or topic is created dynamically by the producer.
1112+
/// By default, the consumer will not retry.
1113+
pub fn init_retries(self, retries: u32, interval: IggyDuration) -> Self {
1114+
Self {
1115+
init_retries: Some(retries),
1116+
init_retry_interval: interval,
1117+
..self
1118+
}
1119+
}
1120+
10161121
/// Allows replaying the messages, `false` by default.
10171122
pub fn allow_replay(self) -> Self {
10181123
Self {
@@ -1040,6 +1145,8 @@ impl IggyConsumerBuilder {
10401145
self.create_consumer_group_if_not_exists,
10411146
self.encryptor,
10421147
self.reconnection_retry_interval,
1148+
self.init_retries,
1149+
self.init_retry_interval,
10431150
self.allow_replay,
10441151
)
10451152
}

sdk/src/clients/producer.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,19 @@ impl IggyProducer {
116116
return Ok(());
117117
}
118118

119+
let stream_id = self.stream_id.clone();
120+
let topic_id = self.topic_id.clone();
121+
info!("Initializing producer for stream: {stream_id} and topic: {topic_id}...");
119122
self.subscribe_events().await;
120123
let client = self.client.clone();
121124
let client = client.read().await;
122-
if client.get_stream(&self.stream_id).await?.is_none() {
125+
if client.get_stream(&stream_id).await?.is_none() {
123126
if !self.create_stream_if_not_exists {
124127
error!("Stream does not exist and auto-creation is disabled.");
125128
return Err(IggyError::StreamNameNotFound(self.stream_name.clone()));
126129
}
127130

128-
let (name, id) = match self.stream_id.kind {
131+
let (name, id) = match stream_id.kind {
129132
IdKind::Numeric => (
130133
self.stream_name.to_owned(),
131134
Some(self.stream_id.get_u32_value()?),
@@ -136,11 +139,7 @@ impl IggyProducer {
136139
client.create_stream(&name, id).await?;
137140
}
138141

139-
if client
140-
.get_topic(&self.stream_id, &self.topic_id)
141-
.await?
142-
.is_none()
143-
{
142+
if client.get_topic(&stream_id, &topic_id).await?.is_none() {
144143
if !self.create_topic_if_not_exists {
145144
error!("Topic does not exist and auto-creation is disabled.");
146145
return Err(IggyError::TopicNameNotFound(
@@ -172,6 +171,7 @@ impl IggyProducer {
172171
}
173172

174173
self.initialized = true;
174+
info!("Producer has been initialized for stream: {stream_id} and topic: {topic_id}.");
175175
Ok(())
176176
}
177177

0 commit comments

Comments
 (0)