lucasbru commented on code in PR #22636:
URL: https://github.com/apache/kafka/pull/22636#discussion_r3453152126


##########
clients/src/main/java/org/apache/kafka/clients/admin/internals/DescribeStreamsGroupsHandler.java:
##########
@@ -120,6 +131,11 @@ public ApiResult<CoordinatorKey, StreamsGroupDescription> 
handleResponse(
 
             final Set<AclOperation> authorizedOperations = 
validAclOperations(describedGroup.authorizedOperations());
 
+            final StreamsGroupTopologyDescriptionStatus 
topologyDescriptionStatus =
+                
StreamsGroupTopologyDescriptionStatus.forId(describedGroup.topologyDescriptionStatus());

Review Comment:
   `forId(byte)` throws `IllegalArgumentException` for any byte not in 
{0,1,2,3}, and there is no try/catch around this call inside `handleResponse`. 
If a broker sends an unknown status code (e.g. a future version adds status 4), 
the exception propagates through `AdminApiDriver.onResponse` -> 
`KafkaAdminClient.call.fail`, which since `IllegalArgumentException` is not 
`RetriableException` immediately fails every key in the batch. Should be caught 
and translated into a per-group failure (put into `failed`), or `forId` should 
have a safe fallback for unknown values.



##########
clients/src/testFixtures/java/org/apache/kafka/clients/admin/MockAdminClient.java:
##########
@@ -1470,9 +1472,27 @@ public synchronized DeleteShareGroupsResult 
deleteShareGroups(Collection<String>
         throw new UnsupportedOperationException("Not implemented yet");
     }
 
+    /**
+     * Registers a {@link StreamsGroupDescription} to be returned by {@link 
#describeStreamsGroups}.
+     */
+    public synchronized void 
addStreamsGroupDescription(StreamsGroupDescription description) {
+        streamsGroupDescriptions.put(description.groupId(), description);
+    }
+
     @Override
     public synchronized DescribeStreamsGroupsResult 
describeStreamsGroups(Collection<String> groupIds, DescribeStreamsGroupsOptions 
options) {

Review Comment:
   `options.includeTopologyDescription()` is never checked. A test that 
registers a description with `status = AVAILABLE` and calls 
`describeStreamsGroups` without `includeTopologyDescription(true)` will get 
`AVAILABLE` back, which a real broker would never do (it returns 
`NOT_REQUESTED`). Tests using this mock cannot catch callers that forget to set 
the option. Should probably return `NOT_REQUESTED` when the option is false.



##########
clients/src/main/java/org/apache/kafka/clients/admin/internals/DescribeStreamsGroupsHandler.java:
##########
@@ -189,6 +207,84 @@ private Map<String, 
StreamsGroupSubtopologyDescription.TopicInfo> convertTopicIn
         ));
     }
 
+    private Optional<StreamsGroupTopologyDescription> 
convertTopologyDescription(
+            final StreamsGroupTopologyDescriptionStatus status,
+            final StreamsGroupDescribeResponseData.TopologyDescription 
topologyDescription) {
+        // The description is present if and only if the status is AVAILABLE.
+        if (status != StreamsGroupTopologyDescriptionStatus.AVAILABLE || 
topologyDescription == null) {
+            return Optional.empty();
+        }
+        final List<StreamsGroupTopologyDescription.Subtopology> subtopologies 
= topologyDescription.subtopologies().stream()
+            .map(this::convertTopologySubtopology)
+            .collect(Collectors.toList());
+        final List<StreamsGroupTopologyDescription.GlobalStore> globalStores = 
topologyDescription.globalStores().stream()
+            .map(this::convertGlobalStore)
+            .collect(Collectors.toList());
+        return Optional.of(new StreamsGroupTopologyDescription(subtopologies, 
globalStores));
+    }
+
+    private StreamsGroupTopologyDescription.Subtopology 
convertTopologySubtopology(
+            final 
StreamsGroupDescribeResponseData.TopologyDescriptionSubtopology subtopology) {
+        final Map<String, Set<String>> predecessors = 
reconstructPredecessors(subtopology.nodes());
+        final List<StreamsGroupTopologyDescription.Node> nodes = 
subtopology.nodes().stream()
+            .map(node -> convertTopologyNode(node, predecessors))
+            .collect(Collectors.toList());
+        return new 
StreamsGroupTopologyDescription.Subtopology(subtopology.subtopologyId(), nodes);
+    }
+
+    private StreamsGroupTopologyDescription.GlobalStore convertGlobalStore(
+            final 
StreamsGroupDescribeResponseData.TopologyDescriptionGlobalStore globalStore) {
+        // The source and processor of a global store form a single unit; 
reconstruct predecessors across the pair.
+        final List<StreamsGroupDescribeResponseData.TopologyDescriptionNode> 
pair =
+            List.of(globalStore.source(), globalStore.processor());
+        final Map<String, Set<String>> predecessors = 
reconstructPredecessors(pair);
+        final StreamsGroupTopologyDescription.Node source = 
convertTopologyNode(globalStore.source(), predecessors);
+        final StreamsGroupTopologyDescription.Node processor = 
convertTopologyNode(globalStore.processor(), predecessors);
+        if (!(source instanceof StreamsGroupTopologyDescription.Source)
+                || !(processor instanceof 
StreamsGroupTopologyDescription.Processor)) {

Review Comment:
   `convertGlobalStore` throws `IllegalStateException` when the 
source/processor node has an unexpected type, and `convertTopologyNode`'s 
default switch arm does the same for unknown `nodeType` values. Both are called 
from inside the `handleResponse` group loop with no catch. Same propagation 
path as the `forId` issue: the exception escapes `handleResponse` and fails 
every key in the batch rather than just the affected group. These should put 
the offending group in `failed` and continue.



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