hubcio commented on code in PR #3525:
URL: https://github.com/apache/iggy/pull/3525#discussion_r3509371521


##########
core/sdk/src/clients/consumer.rs:
##########
@@ -676,6 +702,13 @@ impl IggyConsumer {
                 sleep(retry_interval.get_duration()).await;
             }
 
+            let effective_pid = partition_id.unwrap_or(u32::MAX);
+            let polling_strategy = if 
fallback_to_last.contains_key(&effective_pid) {
+                PollingStrategy::last()

Review Comment:
   the PR title and description say fall back to the *first* available offset 
(`PollingStrategy::first()`), but the code uses `last()` - these are opposites. 
`first()` replays from the earliest retained message (at-least-once, nothing 
avoidable lost); `last()` jumps to the tail and drops the entire still-retained 
backlog. for the sink use case that's silent data loss. if the fallback stays 
it should be `first()`, and disarming on any non-error poll (rather than 
non-empty) avoids a re-arm loop when the tail was already consumed.



##########
core/sdk/src/clients/consumer.rs:
##########
@@ -530,7 +537,17 @@ impl IggyConsumer {
             &self.consumer_name,
             self.joined_consumer_group.clone(),
         )
-        .await
+        .await?;
+
+        // A brand-new consumer group starts at stored offset 0. If the topic 
has
+        // had retention run, offset 0 may no longer exist. Pre-arm the 
fallback
+        // so the very first poll uses PollingStrategy::last() instead of Next,
+        // avoiding a guaranteed InvalidOffset error on startup.
+        if newly_created {

Review Comment:
   the premise here doesn't hold on current master: `Next` with no stored 
offset starts from the first available segment (see the `PollingKind::Next` arm 
in `partitions/ops.rs`), it never returns `InvalidOffset`. so there's no 
startup error to avoid - but the pre-arm itself now forces every freshly 
created consumer group's first poll to `last()`, which silently skips the whole 
existing backlog and (with auto-commit) commits past it, permanently. that's a 
behavior change for every new consumer group, retention or not. the connector 
sinks use exactly this config (consumer group, no partition id, `next()`, 
auto-commit on poll, create-if-not-exists), so a fresh sink deploy against a 
topic with existing messages drops everything except the last batch. same 
pre-arm fires on the rejoin path below, and since the armed flag survives empty 
polls, a group created against an empty topic also skips history if more than a 
batch accumulates before its first non-empty poll. dropping the pre-arm enti
 rely seems right - the server already handles the fresh-group case.



##########
core/sdk/src/clients/consumer.rs:
##########
@@ -111,6 +111,12 @@ pub struct IggyConsumer {
     topic_id: Arc<Identifier>,
     partition_id: Option<u32>,
     polling_strategy: PollingStrategy,
+    // Per-partition recovery set: when a partition's stored offset falls below
+    // its earliest available offset, its ID is inserted here so the next poll
+    // for that partition uses PollingStrategy::last(). Removed after the first
+    // successful recovery poll. Keyed by partition_id; u32::MAX is the 
sentinel
+    // for consumers with no fixed partition (consumer-group auto-assign).
+    fallback_to_last: Arc<DashMap<u32, ()>>,

Review Comment:
   `effective_pid` is constant for the consumer's lifetime (`partition_id` is 
set once in the constructor and never reassigned), so this map holds at most 
one key - it's functionally a bool. the PR description says `Arc<AtomicBool>`, 
which is the right call: simpler, lock-free, and it removes the `u32::MAX` 
sentinel entirely (which today silently no-ops for a consumer group built with 
an explicit partition id, since the pre-arm inserts `u32::MAX` but the poll 
checks the real partition id). the 'per-partition' comment above doesn't match 
what the code can do.



##########
core/sdk/src/clients/consumer.rs:
##########
@@ -783,6 +817,18 @@ impl IggyConsumer {
             let error = polled_messages.unwrap_err();
             error!("Failed to poll messages: {error}");
 
+            // When the consumer group's stored offset falls below the topic's
+            // earliest available offset (e.g. after retention removes old
+            // segments), seek to the most recent message on the next poll
+            // instead of looping forever at the invalid offset.
+            if matches!(error, IggyError::InvalidOffset(_)) {
+                warn!(
+                    "Consumer offset is before the earliest available message 
in topic: {topic_id}, stream: {stream_id}. \

Review Comment:
   this states a cause the server can't produce - `InvalidOffset` fires for an 
offset beyond the end or an empty partition, never for 'before the earliest 
available' (the server skip-forwards that case). and 'falling back to latest' 
contradicts the PR title's 'first available offset'. worth making the text 
neutral about the cause.



##########
core/sdk/src/clients/consumer.rs:
##########
@@ -783,6 +817,18 @@ impl IggyConsumer {
             let error = polled_messages.unwrap_err();
             error!("Failed to poll messages: {error}");
 
+            // When the consumer group's stored offset falls below the topic's
+            // earliest available offset (e.g. after retention removes old
+            // segments), seek to the most recent message on the next poll
+            // instead of looping forever at the invalid offset.
+            if matches!(error, IggyError::InvalidOffset(_)) {

Review Comment:
   on current master this branch is unreachable: the only place the server 
produces `InvalidOffset` is `validate_partition_offset`, which is only called 
from the store-consumer-offset path - the poll path skip-forwards or returns 
empty for out-of-range offsets instead of erroring, and the retention tests in 
`message_cleanup_scenario.rs` poll `offset(0)` after segments were deleted and 
`.unwrap()` successfully. so against a current server this recovery never fires 
and only the pre-arm behavior change remains. also worth re-checking the root 
cause: `Invalid offset: 0` matches the store-path rejection for an empty 
partition (`messages_count == 0 && current_offset == 0`), not a retention purge 
- the production loop was probably an older server or the store path. worth 
verifying against the actual released version before shipping a client-side 
workaround.



##########
core/sdk/src/clients/consumer.rs:
##########
@@ -880,7 +930,10 @@ impl IggyConsumer {
                     return Err(error);
                 }
             }
-        }
+            true

Review Comment:
   if two consumers race to create the same group, the loser hits 
`ConsumerGroupNameAlreadyExists` above and still ends up with `newly_created = 
true` here - it didn't create anything. with the pre-arm this means both 
consumers arm the fallback. the `AlreadyExists` arm should yield `false`.



##########
core/sdk/src/clients/consumer.rs:
##########
@@ -676,6 +702,13 @@ impl IggyConsumer {
                 sleep(retry_interval.get_duration()).await;
             }
 
+            let effective_pid = partition_id.unwrap_or(u32::MAX);
+            let polling_strategy = if 
fallback_to_last.contains_key(&effective_pid) {

Review Comment:
   for a consumer group without a fixed partition, `effective_pid` is always 
`u32::MAX`, but the server picks a (potentially different) partition per poll 
via round-robin. the armed flag doesn't follow the partition that had the 
problem - the `last()` poll lands on whichever partition the server picks next, 
so on a multi-partition topic exactly one (nondeterministic) partition gets 
tail-skipped while the others start from earliest. inconsistent starting state 
within one group, and on servers that do return `InvalidOffset` from poll this 
can repeatedly skip healthy partitions.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to