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


##########
core/sdk/src/http/consumer_offsets.rs:
##########
@@ -101,3 +104,12 @@ impl ConsumerOffsetClient for HttpClient {
 fn get_path(stream_id: &str, topic_id: &str) -> String {
     format!("streams/{stream_id}/topics/{topic_id}/consumer-offsets")
 }
+
+/// The REST API cannot name a consumer kind, so an external group would land 
on a plain
+/// consumer's offset.
+pub(crate) fn refuse_external_group(consumer: &Consumer) -> Result<(), 
IggyError> {

Review Comment:
   warning: HTTP drops the consumer kind, so `ConsumerGroup` offset calls and 
polls also silently act as a plain consumer - the same reason this guard 
exists. refuse everything but `ConsumerKind::Consumer` here, or open a 
follow-up.



##########
core/server/src/dispatch/partition.rs:
##########
@@ -885,25 +890,37 @@ where
     let partition_id = wire.partition_id.unwrap_or(0);
     let namespace =
         resolve_partition_namespace(shard, &wire.stream_id, &wire.topic_id, 
Some(partition_id))?;
+    if wire.consumer.kind != KIND_CONSUMER_GROUP && wire.consumer.kind != 
KIND_EXTERNAL_GROUP {
+        let consumer = polling_consumer_from_wire(&wire.consumer, 
partition_id)?;
+        return Ok(Some((
+            namespace,
+            partition_id,
+            PartitionRead::ConsumerOffset { consumer },
+        )));
+    }
     // A group offset is keyed by the group's monotonic id (any client may read
     // it, member or not), the same key the write path is rewritten to. An
     // unresolved group (e.g. deleted) has no offset, so the read reports None.
-    let consumer = if wire.consumer.kind == KIND_CONSUMER_GROUP {
-        let Some(group_id) = shard
-            .plane
-            .metadata()
-            .mux_stm
-            .streams()
-            .resolve_consumer_group_id(&wire.stream_id, &wire.topic_id, 
&wire.consumer.id)
-        else {
-            return Ok(None);
-        };
-        #[allow(clippy::cast_possible_truncation)]
-        PollingConsumer::ConsumerGroup(group_id as usize, partition_id as 
usize)
+    let Some(group_id) = shard
+        .plane
+        .metadata()
+        .mux_stm
+        .streams()
+        .resolve_consumer_group_id(&wire.stream_id, &wire.topic_id, 
&wire.consumer.id)
+    else {
+        return Ok(None);
+    };
+    #[allow(clippy::cast_possible_truncation)]
+    let read = if wire.consumer.kind == KIND_EXTERNAL_GROUP {
+        PartitionRead::ExternalGroupOffset {

Review Comment:
   warning: the SDK sends this read to the metadata leader, which can be a 
lagging backup for this partition, so a kafka `OffsetFetch` right after 
`OffsetCommit` can return the old offset. route it to the primary or document 
the follower read.



##########
core/sdk/src/stream_builder/build/build_iggy_consumer.rs:
##########
@@ -58,6 +58,8 @@ pub(crate) async fn build_iggy_consumer(
     let mut builder = match consumer_kind {
         ConsumerKind::Consumer => client.consumer(consumer_name, stream, 
topic, partition)?,
         ConsumerKind::ConsumerGroup => client.consumer_group(consumer_name, 
stream, topic)?,
+        // Holds offsets only, it cannot poll.
+        ConsumerKind::ExternalGroup => return 
Err(IggyError::InvalidConfiguration),

Review Comment:
   nit: this rejects `ExternalGroup` only after the stream, topic or producer 
was already built, and returns a bare `InvalidConfiguration` with no log. 
reject it before any server call and log which field is wrong.



##########
core/integration/tests/server/cg.rs:
##########
@@ -81,6 +81,14 @@ async fn offset_cleanup(harness: &TestHarness) {
     consumer_group_offset_cleanup_scenario::run(harness).await;
 }
 
+#[iggy_harness(
+    test_client_transport = [Tcp, WebSocket, Quic],

Review Comment:
   nit: `Http` is left out here, so nothing tests the client-side 
`refuse_external_group` guard or the CLI's `--kind external-group`. add a unit 
test for the guard and a CLI set-then-get case.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -8099,6 +8062,11 @@ where
             .len();
         self.evict_committed_prefix(fenced_prefix).await;
 
+        if !offset_dirs_synced {

Review Comment:
   warning: any sync error other than `NotFound` leaves the purge unrecorded, 
so the reconciler's retry wipes sends this replica applied since - peers keep 
them. when the chain is already reset, retry only the dir sync and the 
`purge.gen` write.
   
   also at line 8099.



##########
core/common/src/types/consumer/consumer_kind.rs:
##########
@@ -88,15 +91,55 @@ impl Consumer {
             id,
         }
     }
+
+    /// Creates a new `ExternalGroup` from the `Identifier` of an Iggy 
consumer group.

Review Comment:
   nit: these docs say external groups are never polled but don't name the 
errors - HTTP calls and polls both return `FeatureUnavailable`. note the HTTP 
case here and on `ConsumerOffsetClient`, and the poll case on 
`MessageClient::poll_messages`.



##########
core/partitions/src/partition_storage.rs:
##########
@@ -798,7 +838,11 @@ async fn load_partition_consumer_group_offsets<S: 
DurableStorage>(
             Ok(RecoveredOffsets::default())
         }
         Err(source) => Err(PartitionRecoveryError::ConsumerOffsetsLoad {
-            consumer_kind: "consumer group",
+            consumer_kind: if kind == ConsumerKind::ExternalGroup {

Review Comment:
   simplification: pick the label with `kind.as_str()` instead of this if/else, 
and use `ConsumerKind::ExternalGroup.as_str()` at line 304 - drops the 
duplicated literal. the message text becomes `consumer_group`, which no test or 
doc depends on.



##########
core/partitions/src/state_transfer.rs:
##########
@@ -2092,17 +2146,14 @@ where
                 .retain(|start_offset, _| live.contains_key(start_offset));
         }
 
-        // An empty chain at frontier 0 tells the receiver to unlink its own, 
so
-        // serve it only when a recorded purge says the emptiness is the truth.
-        // `install_state_transfer`'s `purge_advances` check re-decides that
-        // against the metadata plane and refuses the rest.
+        // An empty chain at frontier 0 is still served. The `NothingCommitted`

Review Comment:
   warning: without the old `NothingCommitted` check, a zero-byte replica whose 
floor moved through an offsets-only window serves an empty chain, and a 
receiver at frontier 0 installs it, claiming history it doesn't hold. keep 
refusing when nothing backs the floor.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -1106,14 +1095,8 @@ where
                 }
             }
         }
-        let mut directories = Vec::with_capacity(4);
-        for directory in [
-            &self.consumer_offsets_path,
-            &self.consumer_group_offsets_path,
-        ]
-        .into_iter()
-        .flatten()
-        {
+        let mut directories = Vec::with_capacity(ConsumerKind::COUNT + 2);
+        for directory in self.consumer_offset_dirs.iter().flatten() {

Review Comment:
   nit: every WAL checkpoint now syncs a fifth directory, even with no external 
offsets. only multi-replica `Persisted` partitions pay it, so it's minor - skip 
`external_groups/` while it's empty, or leave it.



##########
core/simulator/src/storage/purge.rs:
##########
@@ -25,7 +25,12 @@
 //! The two controls make that distinction observable. Without a purge, 
recovery
 //! must preserve bookmark 2 and return messages 3 and 4. After a completed 
purge,
 //! recovery must find no bookmarks and return all five messages, 0 through 4.
-//! Both controls cover individual consumers and groups under both offset 
policies.
+//! Both controls cover individual consumers, groups, and external groups under
+//! both offset policies. An external group is never polled, so its bookmark is
+//! read directly. Boot never clamps it, so a resurrected one would survive.
+//!
+//! A third test purges after one consumer or group directory is gone, a state
+//! that recovery accepts. The purge must still record its generation.

Review Comment:
   nit: the new test only covers the `NotFound` case. nothing drives the other 
sync-failure arm, where the generation stays unrecorded and the purge gets 
retried - add a test that fails one offsets dir sync and checks the retry.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -752,8 +747,9 @@ where
 
     /// Set the per-kind durable consumer-offset limit for this partition.
     pub fn set_consumer_offsets_max(&mut self, offsets_max: usize) {
-        self.consumer_offset_capacity.set_limit(offsets_max);
-        self.consumer_group_offset_capacity.set_limit(offsets_max);
+        for capacity in &self.consumer_offset_capacities {
+            capacity.set_limit(offsets_max);

Review Comment:
   nit: every kind gets its own `consumer_offsets_max` budget here, so external 
groups add a third, but the comment at `core/server/config.toml:983` still 
talks about two kinds. mention external groups there so operators size for 
three.



-- 
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