This is an automated email from the ASF dual-hosted git repository.

lucasbru pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new a3362c491e9 KAFKA-20625: Add 
StreamsGroupTopologyDescriptionRequestManager and Streams client topology push 
[2/N] (#22640)
a3362c491e9 is described below

commit a3362c491e9d36b4dcc4827232d3e11af50a20c4
Author: Lucy Liu <[email protected]>
AuthorDate: Wed Jun 24 14:16:34 2026 -0500

    KAFKA-20625: Add StreamsGroupTopologyDescriptionRequestManager and Streams 
client topology push [2/N] (#22640)
    
    ## Summary
    Adds the client-side request manager that pushes topology descriptions
    to the broker, wires the heartbeat path to flip the push-required flag
    on responses, and registers the new manager in the consumer network
    thread. Should be based on https://github.com/apache/kafka/pull/22639
    
    ## File changes
    - **`StreamsGroupTopologyDescriptionRequestManager`**: new
    RequestManager that implements the KIP-1331 response handling with unit
    tests.
    - **`StreamsGroupHeartbeatRequestManager`**: `onSuccessResponse`
    propagates `memberId` and sets `topologyPushRequired` when the broker
    requests a push, with unit tests.
    - **`RequestManagers`**: registers the new manager in the Streams branch
    and the entries list. Current tests updated.
    - **`StreamThread.initStreamsRebalanceData`**: bumped to package-private
    for testing. 2 new tests verify the wire description is
    populated/skipped based on the config.
    
    Reviewers: Lucas Brutschy <[email protected]>
    
    ---------
    
    Co-authored-by: Lucas Brutschy <[email protected]>
---
 .../consumer/internals/RequestManagers.java        |  17 +
 .../StreamsGroupHeartbeatRequestManager.java       |   6 +
 ...eamsGroupTopologyDescriptionRequestManager.java | 181 +++++++++++
 .../consumer/internals/RequestManagersTest.java    |   8 +-
 .../StreamsGroupHeartbeatRequestManagerTest.java   |  76 +++++
 ...GroupTopologyDescriptionRequestManagerTest.java | 342 +++++++++++++++++++++
 .../events/ApplicationEventProcessorTest.java      |   4 +
 .../streams/processor/internals/StreamThread.java  |   5 +-
 .../processor/internals/StreamThreadTest.java      |  46 +++
 9 files changed, 681 insertions(+), 4 deletions(-)

diff --git 
a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/RequestManagers.java
 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/RequestManagers.java
index 36ebbb3bbf3..e93da12cda6 100644
--- 
a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/RequestManagers.java
+++ 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/RequestManagers.java
@@ -61,6 +61,7 @@ public class RequestManagers implements Closeable {
     public final FetchRequestManager fetchRequestManager;
     public final Optional<ShareConsumeRequestManager> 
shareConsumeRequestManager;
     public final Optional<StreamsGroupHeartbeatRequestManager> 
streamsGroupHeartbeatRequestManager;
+    public final Optional<StreamsGroupTopologyDescriptionRequestManager> 
streamsGroupTopologyDescriptionRequestManager;
     private final List<RequestManager> entries;
     private final IdempotentCloser closer = new IdempotentCloser();
 
@@ -73,6 +74,7 @@ public class RequestManagers implements Closeable {
                            Optional<ConsumerHeartbeatRequestManager> 
heartbeatRequestManager,
                            Optional<ConsumerMembershipManager> 
membershipManager,
                            Optional<StreamsGroupHeartbeatRequestManager> 
streamsGroupHeartbeatRequestManager,
+                           
Optional<StreamsGroupTopologyDescriptionRequestManager> 
streamsGroupTopologyDescriptionRequestManager,
                            Optional<StreamsMembershipManager> 
streamsMembershipManager) {
         this.log = logContext.logger(RequestManagers.class);
         this.offsetsRequestManager = requireNonNull(offsetsRequestManager, 
"OffsetsRequestManager cannot be null");
@@ -84,6 +86,7 @@ public class RequestManagers implements Closeable {
         this.consumerHeartbeatRequestManager = heartbeatRequestManager;
         this.shareHeartbeatRequestManager = Optional.empty();
         this.streamsGroupHeartbeatRequestManager = 
streamsGroupHeartbeatRequestManager;
+        this.streamsGroupTopologyDescriptionRequestManager = 
streamsGroupTopologyDescriptionRequestManager;
         this.consumerMembershipManager = membershipManager;
         this.streamsMembershipManager = streamsMembershipManager;
         this.shareMembershipManager = Optional.empty();
@@ -94,6 +97,7 @@ public class RequestManagers implements Closeable {
         heartbeatRequestManager.ifPresent(list::add);
         membershipManager.ifPresent(list::add);
         streamsGroupHeartbeatRequestManager.ifPresent(list::add);
+        streamsGroupTopologyDescriptionRequestManager.ifPresent(list::add);
         streamsMembershipManager.ifPresent(list::add);
         list.add(offsetsRequestManager);
         list.add(topicMetadataRequestManager);
@@ -112,6 +116,7 @@ public class RequestManagers implements Closeable {
         this.commitRequestManager = Optional.empty();
         this.consumerHeartbeatRequestManager = Optional.empty();
         this.streamsGroupHeartbeatRequestManager = Optional.empty();
+        this.streamsGroupTopologyDescriptionRequestManager = Optional.empty();
         this.shareHeartbeatRequestManager = shareHeartbeatRequestManager;
         this.consumerMembershipManager = Optional.empty();
         this.streamsMembershipManager = Optional.empty();
@@ -199,6 +204,7 @@ public class RequestManagers implements Closeable {
                 CoordinatorRequestManager coordinator = null;
                 CommitRequestManager commitRequestManager = null;
                 StreamsGroupHeartbeatRequestManager 
streamsGroupHeartbeatRequestManager = null;
+                StreamsGroupTopologyDescriptionRequestManager 
streamsGroupTopologyDescriptionRequestManager = null;
                 StreamsMembershipManager streamsMembershipManager = null;
 
                 if (groupRebalanceConfig != null && 
groupRebalanceConfig.groupId != null) {
@@ -247,6 +253,16 @@ public class RequestManagers implements Closeable {
                             metrics,
                             streamsRebalanceData.get()
                         );
+
+                        streamsGroupTopologyDescriptionRequestManager = new 
StreamsGroupTopologyDescriptionRequestManager(
+                            logContext,
+                            time,
+                            retryBackoffMs,
+                            retryBackoffMaxMs,
+                            streamsMembershipManager,
+                            streamsRebalanceData.get(),
+                            coordinator
+                        );
                     } else {
                         membershipManager = new ConsumerMembershipManager(
                             groupRebalanceConfig.groupId,
@@ -308,6 +324,7 @@ public class RequestManagers implements Closeable {
                         Optional.ofNullable(heartbeatRequestManager),
                         Optional.ofNullable(membershipManager),
                         
Optional.ofNullable(streamsGroupHeartbeatRequestManager),
+                        
Optional.ofNullable(streamsGroupTopologyDescriptionRequestManager),
                         Optional.ofNullable(streamsMembershipManager)
                 );
             }
diff --git 
a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java
 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java
index 4793025f88c..fce71fe95d5 100644
--- 
a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java
+++ 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java
@@ -571,6 +571,10 @@ public class StreamsGroupHeartbeatRequestManager 
implements RequestManager {
         
streamsRebalanceData.setTaskOffsetIntervalMs(data.taskOffsetIntervalMs());
         
streamsRebalanceData.setAcceptableRecoveryLag(data.acceptableRecoveryLag());
 
+        if (data.topologyDescriptionRequired() && 
streamsRebalanceData.wireTopologyDescription() != null) {
+            streamsRebalanceData.setTopologyPushRequired(true);
+        }
+
         if (data.partitionsByUserEndpoint() != null) {
             streamsRebalanceData.setPartitionsByHost(convertHostInfoMap(data));
         }
@@ -673,6 +677,7 @@ public class StreamsGroupHeartbeatRequestManager implements 
RequestManager {
                 membershipManager.onFenced();
                 // Skip backoff so that a next HB to rejoin is sent as soon as 
the fenced member releases its assignment
                 heartbeatRequestState.reset();
+                streamsRebalanceData.setTopologyPushRequired(false);
                 break;
 
             case UNKNOWN_MEMBER_ID:
@@ -685,6 +690,7 @@ public class StreamsGroupHeartbeatRequestManager implements 
RequestManager {
                 membershipManager.onFenced();
                 // Skip backoff so that a next HB to rejoin is sent as soon as 
the fenced member releases its assignment
                 heartbeatRequestState.reset();
+                streamsRebalanceData.setTopologyPushRequired(false);
                 break;
 
             case UNSUPPORTED_VERSION:
diff --git 
a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupTopologyDescriptionRequestManager.java
 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupTopologyDescriptionRequestManager.java
new file mode 100644
index 00000000000..30979905693
--- /dev/null
+++ 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupTopologyDescriptionRequestManager.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.kafka.clients.consumer.internals;
+
+import org.apache.kafka.clients.ClientResponse;
+import org.apache.kafka.common.errors.RetriableException;
+import 
org.apache.kafka.common.message.StreamsGroupTopologyDescriptionUpdateRequestData;
+import org.apache.kafka.common.protocol.Errors;
+import 
org.apache.kafka.common.requests.StreamsGroupTopologyDescriptionUpdateRequest;
+import 
org.apache.kafka.common.requests.StreamsGroupTopologyDescriptionUpdateResponse;
+import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.common.utils.internals.LogContext;
+
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.Objects;
+
+public class StreamsGroupTopologyDescriptionRequestManager implements 
RequestManager {
+
+    private final Logger logger;
+    private final Time time;
+    private final StreamsMembershipManager membershipManager;
+    private final StreamsRebalanceData streamsRebalanceData;
+    private final CoordinatorRequestManager coordinatorRequestManager;
+    private final RequestState pushRequestState;
+
+    private long nextPushTimeMs = 0L;
+
+    public StreamsGroupTopologyDescriptionRequestManager(final LogContext 
logContext,
+                                                         final Time time,
+                                                         final long 
retryBackoffMs,
+                                                         final long 
retryBackoffMaxMs,
+                                                         final 
StreamsMembershipManager membershipManager,
+                                                         final 
StreamsRebalanceData streamsRebalanceData,
+                                                         final 
CoordinatorRequestManager coordinatorRequestManager) {
+        this.logger = logContext.logger(getClass());
+        this.time = Objects.requireNonNull(time);
+        this.membershipManager = Objects.requireNonNull(membershipManager);
+        this.streamsRebalanceData = 
Objects.requireNonNull(streamsRebalanceData);
+        this.coordinatorRequestManager = 
Objects.requireNonNull(coordinatorRequestManager);
+        this.pushRequestState = new RequestState(
+            logContext,
+            
StreamsGroupTopologyDescriptionRequestManager.class.getSimpleName(),
+            retryBackoffMs,
+            retryBackoffMaxMs);
+    }
+
+    @Override
+    public NetworkClientDelegate.PollResult poll(final long currentTimeMs) {
+        if (!shouldSendTopologyDescriptionUpdate(currentTimeMs)) {
+            return NetworkClientDelegate.PollResult.EMPTY;
+        }
+
+        final StreamsGroupTopologyDescriptionUpdateRequestData data = new 
StreamsGroupTopologyDescriptionUpdateRequestData()
+            .setGroupId(membershipManager.groupId())
+            .setMemberId(membershipManager.memberId())
+            .setTopologyEpoch(streamsRebalanceData.topologyEpoch())
+            
.setTopologyDescription(streamsRebalanceData.wireTopologyDescription());
+
+        final NetworkClientDelegate.UnsentRequest unsent = new 
NetworkClientDelegate.UnsentRequest(
+            new StreamsGroupTopologyDescriptionUpdateRequest.Builder(data),
+            coordinatorRequestManager.coordinator()
+        );
+        unsent.whenComplete((response, exception) -> onResponse(response, 
exception));
+
+        pushRequestState.onSendAttempt(currentTimeMs);
+        return new 
NetworkClientDelegate.PollResult(Collections.singletonList(unsent));
+    }
+
+    @Override
+    public long maximumTimeToWait(final long currentTimeMs) {
+        if (!streamsRebalanceData.topologyPushRequired()) {
+            return Long.MAX_VALUE;
+        }
+        final long backoffRemainingMs = 
pushRequestState.remainingBackoffMs(currentTimeMs);
+        final long throttleRemainingMs = Math.max(0L, nextPushTimeMs - 
currentTimeMs);
+        final long waitMs = Math.max(backoffRemainingMs, throttleRemainingMs);
+        if (waitMs > 0L) {
+            return waitMs;
+        }
+        return shouldSendTopologyDescriptionUpdate(currentTimeMs) ? 0L : 
Long.MAX_VALUE;
+    }
+
+    private boolean shouldSendTopologyDescriptionUpdate(final long 
currentTimeMs) {
+        if (!pushRequestState.canSendRequest(currentTimeMs) || currentTimeMs < 
nextPushTimeMs) {
+            return false;
+        }
+        if (!streamsRebalanceData.topologyPushRequired() || 
streamsRebalanceData.wireTopologyDescription() == null) {
+            return false;
+        }
+        final String memberId = membershipManager.memberId();
+        if (memberId == null || memberId.isEmpty()) {
+            return false;
+        }
+        return coordinatorRequestManager.coordinator().isPresent();
+    }
+
+    private void onResponse(final ClientResponse response, final Throwable 
exception) {
+        final long responseTimeMs = time.milliseconds();
+
+        if (exception != null) {
+            if (exception instanceof RetriableException) {
+                pushRequestState.onFailedAttempt(responseTimeMs);
+                
coordinatorRequestManager.handleCoordinatorDisconnect(exception, 
responseTimeMs);
+                logger.warn("Topology description push failed with retriable 
exception; will retry on next poll", exception);
+            } else {
+                // Non-retriable exceptions should clear the flag and give up.
+                pushRequestState.onSuccessfulAttempt(responseTimeMs);
+                streamsRebalanceData.setTopologyPushRequired(false);
+                logger.warn("Topology description push failed with 
non-retriable exception.", exception);
+            }
+            return;
+        }
+
+        final StreamsGroupTopologyDescriptionUpdateResponse body =
+            (StreamsGroupTopologyDescriptionUpdateResponse) 
response.responseBody();
+        final Errors error = Errors.forCode(body.data().errorCode());
+        final String errorMessage = body.data().errorMessage();
+
+        if (body.data().throttleTimeMs() > 0) {
+            nextPushTimeMs = responseTimeMs + body.data().throttleTimeMs();
+        }
+
+        switch (error) {
+            case NONE:
+                pushRequestState.onSuccessfulAttempt(responseTimeMs);
+                streamsRebalanceData.setTopologyPushRequired(false);
+                break;
+
+            case NOT_COORDINATOR:
+            case COORDINATOR_NOT_AVAILABLE:
+                pushRequestState.onFailedAttempt(responseTimeMs);
+                logger.info("Coordinator error {} pushing topology 
description. Will rediscover and retry: {}", error, errorMessage);
+                coordinatorRequestManager.markCoordinatorUnknown(errorMessage, 
responseTimeMs);
+                break;
+
+            case COORDINATOR_LOAD_IN_PROGRESS:
+                pushRequestState.onFailedAttempt(responseTimeMs);
+                logger.info("Coordinator is loading; will retry on next poll: 
{}", errorMessage);
+                break;
+
+            case UNKNOWN_MEMBER_ID:
+                // Member was dropped — clear the flag and let the heartbeat 
path drive the rejoin.
+                // onSuccessfulAttempt resets request state without backoff 
since no retry follows.
+                pushRequestState.onSuccessfulAttempt(responseTimeMs);
+                logger.info("Topology description push rejected with 
UNKNOWN_MEMBER_ID; heartbeat will trigger rejoin: {}", errorMessage);
+                streamsRebalanceData.setTopologyPushRequired(false);
+                break;
+
+            case STREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILED:
+            case INVALID_REQUEST:
+            case UNSUPPORTED_VERSION:
+            case GROUP_ID_NOT_FOUND:
+            case GROUP_AUTHORIZATION_FAILED:
+            default:
+                // Use onSuccessfulAttempt because no retry follows,
+                // a future push triggered by a later heartbeat should not 
inherit backoff from this failure.
+                // the broker will re-signal via heartbeat if a push is needed 
again.
+                pushRequestState.onSuccessfulAttempt(responseTimeMs);
+                logger.warn("Topology description push failed with {}: {}", 
error, errorMessage);
+                streamsRebalanceData.setTopologyPushRequired(false);
+                break;
+        }
+    }
+
+}
diff --git 
a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/RequestManagersTest.java
 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/RequestManagersTest.java
index 63e24ca0228..cc74aee7760 100644
--- 
a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/RequestManagersTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/RequestManagersTest.java
@@ -39,7 +39,7 @@ import static org.mockito.Mockito.mock;
 public class RequestManagersTest {
 
     @Test
-    public void testMemberStateListenerRegistered() {
+    public void testConsumerGroupRequestManagersAndListenersWired() {
 
         final MemberStateListener listener = (memberEpoch, memberId) -> { };
 
@@ -77,6 +77,7 @@ public class RequestManagersTest {
         assertTrue(requestManagers.consumerMembershipManager.isPresent());
         assertTrue(requestManagers.streamsMembershipManager.isEmpty());
         
assertTrue(requestManagers.streamsGroupHeartbeatRequestManager.isEmpty());
+        
assertTrue(requestManagers.streamsGroupTopologyDescriptionRequestManager.isEmpty());
 
         assertEquals(2, 
requestManagers.consumerMembershipManager.get().stateListeners().size());
         
assertTrue(requestManagers.consumerMembershipManager.get().stateListeners().stream()
@@ -85,7 +86,7 @@ public class RequestManagersTest {
     }
 
     @Test
-    public void testStreamMemberStateListenerRegistered() {
+    public void testStreamsGroupRequestManagersAndListenersWired() {
 
         final MemberStateListener listener = (memberEpoch, memberId) -> { };
 
@@ -122,6 +123,9 @@ public class RequestManagersTest {
         ).get();
         assertTrue(requestManagers.streamsMembershipManager.isPresent());
         
assertTrue(requestManagers.streamsGroupHeartbeatRequestManager.isPresent());
+        
assertTrue(requestManagers.streamsGroupTopologyDescriptionRequestManager.isPresent());
+        assertTrue(requestManagers.entries().stream()
+            .anyMatch(rm -> rm instanceof 
StreamsGroupTopologyDescriptionRequestManager));
         assertTrue(requestManagers.consumerMembershipManager.isEmpty());
 
         assertEquals(2, 
requestManagers.streamsMembershipManager.get().stateListeners().size());
diff --git 
a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManagerTest.java
 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManagerTest.java
index 9d92d251745..c2083fbafd0 100644
--- 
a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManagerTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManagerTest.java
@@ -29,6 +29,7 @@ import 
org.apache.kafka.common.errors.TopicAuthorizationException;
 import org.apache.kafka.common.errors.UnsupportedVersionException;
 import org.apache.kafka.common.message.StreamsGroupHeartbeatRequestData;
 import org.apache.kafka.common.message.StreamsGroupHeartbeatResponseData;
+import 
org.apache.kafka.common.message.StreamsGroupTopologyDescriptionUpdateRequestData;
 import org.apache.kafka.common.metrics.Metrics;
 import org.apache.kafka.common.protocol.ApiKeys;
 import org.apache.kafka.common.protocol.Errors;
@@ -1712,6 +1713,62 @@ class StreamsGroupHeartbeatRequestManagerTest {
         }
     }
 
+    @Test
+    public void 
testStreamsRebalanceDataTopologyPushRequiredSetOnRequiredTrue() {
+        try (
+            final MockedConstruction<HeartbeatRequestState> ignored = 
mockConstruction(
+                HeartbeatRequestState.class,
+                (mock, context) -> 
when(mock.canSendRequest(time.milliseconds())).thenReturn(true))
+        ) {
+            final StreamsGroupHeartbeatRequestManager heartbeatRequestManager 
= createStreamsGroupHeartbeatRequestManager();
+            
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+            when(membershipManager.groupId()).thenReturn(GROUP_ID);
+            when(membershipManager.memberId()).thenReturn(MEMBER_ID);
+            when(membershipManager.memberEpoch()).thenReturn(MEMBER_EPOCH);
+            
when(membershipManager.groupInstanceId()).thenReturn(Optional.of(INSTANCE_ID));
+
+            // Push is enabled (wire description present) so the broker's 
required=true flips the flag.
+            streamsRebalanceData.setWireTopologyDescription(
+                new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+            assertFalse(streamsRebalanceData.topologyPushRequired());
+
+            final NetworkClientDelegate.PollResult result = 
heartbeatRequestManager.poll(time.milliseconds());
+            assertEquals(1, result.unsentRequests.size());
+
+            final NetworkClientDelegate.UnsentRequest networkRequest = 
result.unsentRequests.get(0);
+            
networkRequest.handler().onComplete(buildClientResponseWithTopologyRequired(true));
+
+            assertTrue(streamsRebalanceData.topologyPushRequired());
+        }
+    }
+
+    @Test
+    public void 
testStreamsRebalanceDataTopologyPushRequiredNotClearedOnRequiredFalse() {
+        try (
+            final MockedConstruction<HeartbeatRequestState> ignored = 
mockConstruction(
+                HeartbeatRequestState.class,
+                (mock, context) -> 
when(mock.canSendRequest(time.milliseconds())).thenReturn(true))
+        ) {
+            final StreamsGroupHeartbeatRequestManager heartbeatRequestManager 
= createStreamsGroupHeartbeatRequestManager();
+            
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+            when(membershipManager.groupId()).thenReturn(GROUP_ID);
+            when(membershipManager.memberId()).thenReturn(MEMBER_ID);
+            when(membershipManager.memberEpoch()).thenReturn(MEMBER_EPOCH);
+            
when(membershipManager.groupInstanceId()).thenReturn(Optional.of(INSTANCE_ID));
+
+            // Pre-set the flag to true to verify it survives a response that 
doesn't ask for push
+            streamsRebalanceData.setTopologyPushRequired(true);
+
+            final NetworkClientDelegate.PollResult result = 
heartbeatRequestManager.poll(time.milliseconds());
+            assertEquals(1, result.unsentRequests.size());
+
+            final NetworkClientDelegate.UnsentRequest networkRequest = 
result.unsentRequests.get(0);
+            
networkRequest.handler().onComplete(buildClientResponseWithTopologyRequired(false));
+
+            assertTrue(streamsRebalanceData.topologyPushRequired());
+        }
+    }
+
     private static ConsumerConfig config() {
         Properties prop = new Properties();
         prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
StringDeserializer.class);
@@ -1786,4 +1843,23 @@ class StreamsGroupHeartbeatRequestManagerTest {
             .collect(Collectors.toList());
         assertEquals(sortedExpected, sortedActual);
     }
+
+    private ClientResponse buildClientResponseWithTopologyRequired(final 
boolean topologyRequired) {
+        return new ClientResponse(
+            new RequestHeader(ApiKeys.STREAMS_GROUP_HEARTBEAT, (short) 1, "", 
1),
+            null,
+            "-1",
+            time.milliseconds(),
+            time.milliseconds(),
+            false,
+            null,
+            null,
+            new StreamsGroupHeartbeatResponse(
+                new StreamsGroupHeartbeatResponseData()
+                    .setPartitionsByUserEndpoint(ENDPOINT_TO_PARTITIONS)
+                    .setHeartbeatIntervalMs((int) 
RECEIVED_HEARTBEAT_INTERVAL_MS)
+                    .setTopologyDescriptionRequired(topologyRequired)
+            )
+        );
+    }
 }
diff --git 
a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/StreamsGroupTopologyDescriptionRequestManagerTest.java
 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/StreamsGroupTopologyDescriptionRequestManagerTest.java
new file mode 100644
index 00000000000..05eb5d032a3
--- /dev/null
+++ 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/StreamsGroupTopologyDescriptionRequestManagerTest.java
@@ -0,0 +1,342 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.kafka.clients.consumer.internals;
+
+import org.apache.kafka.clients.ClientResponse;
+import org.apache.kafka.common.Node;
+import 
org.apache.kafka.common.message.StreamsGroupTopologyDescriptionUpdateRequestData;
+import 
org.apache.kafka.common.message.StreamsGroupTopologyDescriptionUpdateResponseData;
+import org.apache.kafka.common.protocol.ApiKeys;
+import org.apache.kafka.common.protocol.Errors;
+import org.apache.kafka.common.requests.RequestHeader;
+import 
org.apache.kafka.common.requests.StreamsGroupTopologyDescriptionUpdateRequest;
+import 
org.apache.kafka.common.requests.StreamsGroupTopologyDescriptionUpdateResponse;
+import org.apache.kafka.common.utils.MockTime;
+import org.apache.kafka.common.utils.internals.LogContext;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class StreamsGroupTopologyDescriptionRequestManagerTest {
+    private static final String GROUP_ID = "group-id";
+    private static final String MEMBER_ID = "member-id";
+    private static final long RETRY_BACKOFF_MS = 100;
+    private static final long RETRY_BACKOFF_MAX_MS = 1000;
+
+    private final MockTime time = new MockTime();
+    private final LogContext logContext = new LogContext();
+    private final Node coordinatorNode = new Node(1, "host", 9092);
+
+    private CoordinatorRequestManager coordinatorRequestManager;
+    private StreamsMembershipManager membershipManager;
+    private StreamsRebalanceData streamsRebalanceData;
+    private StreamsGroupTopologyDescriptionRequestManager manager;
+
+    @BeforeEach
+    public void setUp() {
+        coordinatorRequestManager = mock(CoordinatorRequestManager.class);
+        membershipManager = mock(StreamsMembershipManager.class);
+        when(membershipManager.groupId()).thenReturn(GROUP_ID);
+        when(membershipManager.memberId()).thenReturn(MEMBER_ID);
+        streamsRebalanceData = new StreamsRebalanceData(
+            UUID.randomUUID(), Optional.empty(), Optional.empty(), Map.of(), 
Map.of(), Map::of, Map::of
+        );
+        manager = new StreamsGroupTopologyDescriptionRequestManager(
+            logContext, time, RETRY_BACKOFF_MS, RETRY_BACKOFF_MAX_MS,
+            membershipManager, streamsRebalanceData, coordinatorRequestManager
+        );
+    }
+
+    /**
+     * Test the situation when the coordinator has not yet been discovered: no 
request should be sent
+     * and the client should silently wait for the coordinator to become known.
+     */
+    @Test
+    public void testCoordinatorUnknown() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.empty());
+
+        assertEquals(0, 
manager.poll(time.milliseconds()).unsentRequests.size());
+    }
+
+    /**
+     * Test the situation when the wire-format topology description has not 
yet been populated:
+     * no request should be sent.
+     */
+    @Test
+    public void testWireTopologyDescriptionNull() {
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        assertEquals(0, 
manager.poll(time.milliseconds()).unsentRequests.size());
+    }
+
+    /**
+     * Test the situation when memberId is null because the member has not yet 
joined the group:
+     * no request should be sent.
+     */
+    @Test
+    public void testMemberIdNull() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+        when(membershipManager.memberId()).thenReturn(null);
+
+        assertEquals(0, 
manager.poll(time.milliseconds()).unsentRequests.size());
+    }
+
+    /**
+     * Test the situation when the topologyPushRequired flag is not set: 
maximumTimeToWait should
+     * return Long.MAX_VALUE and no request should be sent even if every other 
precondition is satisfied.
+     */
+    @Test
+    public void testTopologyDescriptionWhenPushFlagNotSet() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        assertEquals(Long.MAX_VALUE, 
manager.maximumTimeToWait(time.milliseconds()));
+        assertEquals(0, 
manager.poll(time.milliseconds()).unsentRequests.size());
+    }
+
+    /**
+     * Test the situation when all preconditions are satisfied: 
maximumTimeToWait should return 0
+     * to wake the poll loop immediately, and the subsequent poll should build 
a request with the
+     * correct groupId, memberId, and target coordinator node.
+     */
+    @Test
+    public void testSendRequestWhenAllPreconditionsMet() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        assertEquals(0L, manager.maximumTimeToWait(time.milliseconds()));
+
+        final NetworkClientDelegate.PollResult result = 
manager.poll(time.milliseconds());
+
+        assertEquals(1, result.unsentRequests.size());
+        final NetworkClientDelegate.UnsentRequest unsent = 
result.unsentRequests.get(0);
+        assertEquals(Optional.of(coordinatorNode), unsent.node());
+        final StreamsGroupTopologyDescriptionUpdateRequest request =
+            (StreamsGroupTopologyDescriptionUpdateRequest) 
unsent.requestBuilder().build();
+        assertEquals(GROUP_ID, request.data().groupId());
+        assertEquals(MEMBER_ID, request.data().memberId());
+    }
+
+    /**
+     * Test the situation when a push request is already in flight: subsequent 
polls should not
+     * enqueue duplicate requests.
+     */
+    @Test
+    public void testNoSecondRequestWhileInflight() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        manager.poll(time.milliseconds());
+        assertEquals(0, 
manager.poll(time.milliseconds()).unsentRequests.size());
+    }
+
+    /**
+     * Test the situation when the broker responds with no error: the 
topologyPushRequired flag
+     * should be cleared so the manager stops pushing.
+     */
+    @Test
+    public void testFlagClearedOnSuccess() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        final NetworkClientDelegate.UnsentRequest unsent =
+            manager.poll(time.milliseconds()).unsentRequests.get(0);
+        unsent.handler().onComplete(buildResponse(Errors.NONE, 0));
+
+        assertFalse(streamsRebalanceData.topologyPushRequired());
+    }
+
+    /**
+     * Test the situation when the broker responds with UNKNOWN_MEMBER_ID: the 
flag should be
+     * cleared so the heartbeat path can drive a clean rejoin.
+     */
+    @Test
+    public void testFlagClearedOnUnknownMemberId() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        final NetworkClientDelegate.UnsentRequest unsent =
+            manager.poll(time.milliseconds()).unsentRequests.get(0);
+        unsent.handler().onComplete(buildResponse(Errors.UNKNOWN_MEMBER_ID, 
0));
+
+        assertFalse(streamsRebalanceData.topologyPushRequired());
+    }
+
+    /**
+     * Test the situation when the broker responds with NOT_COORDINATOR or 
COORDINATOR_NOT_AVAILABLE:
+     * coordinator rediscovery should be triggered and the flag should remain 
set for retry.
+     */
+    @Test
+    public void testCoordinatorErrorTriggersRediscovery() {
+        for (final Errors error : new Errors[]{Errors.NOT_COORDINATOR, 
Errors.COORDINATOR_NOT_AVAILABLE}) {
+            streamsRebalanceData.setWireTopologyDescription(
+                new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+            streamsRebalanceData.setTopologyPushRequired(true);
+            
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+            time.sleep(RETRY_BACKOFF_MAX_MS);
+
+            final NetworkClientDelegate.UnsentRequest unsent =
+                manager.poll(time.milliseconds()).unsentRequests.get(0);
+            unsent.handler().onComplete(buildResponse(error, 0));
+
+            assertTrue(streamsRebalanceData.topologyPushRequired(),
+                "Flag should remain set after " + error);
+        }
+        verify(coordinatorRequestManager, 
times(2)).markCoordinatorUnknown(any(), anyLong());
+    }
+
+    /**
+     * Test the situation when the broker responds with 
COORDINATOR_LOAD_IN_PROGRESS: the flag
+     * should remain set for retry but coordinator rediscovery should not be 
triggered, since the
+     * coordinator is correct but still loading.
+     */
+    @Test
+    public void testCoordinatorLoadInProgressDoesNotTriggerRediscovery() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        final NetworkClientDelegate.UnsentRequest unsent =
+            manager.poll(time.milliseconds()).unsentRequests.get(0);
+        
unsent.handler().onComplete(buildResponse(Errors.COORDINATOR_LOAD_IN_PROGRESS, 
0));
+
+        assertTrue(streamsRebalanceData.topologyPushRequired());
+        verify(coordinatorRequestManager, 
never()).markCoordinatorUnknown(any(), anyLong());
+    }
+
+    /**
+     * Test the situation when the broker responds with a terminal error 
listed in KIP-1331: the
+     * flag should be cleared and the client should not retry on its own.
+     */
+    @Test
+    public void testTerminalErrorsClearFlag() {
+        for (final Errors error : new Errors[]{
+            Errors.STREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILED,
+            Errors.INVALID_REQUEST,
+            Errors.UNSUPPORTED_VERSION,
+            Errors.GROUP_ID_NOT_FOUND,
+            Errors.GROUP_AUTHORIZATION_FAILED}) {
+            streamsRebalanceData.setWireTopologyDescription(
+                new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+            streamsRebalanceData.setTopologyPushRequired(true);
+            
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+            time.sleep(RETRY_BACKOFF_MAX_MS);
+
+            final NetworkClientDelegate.UnsentRequest unsent =
+                manager.poll(time.milliseconds()).unsentRequests.get(0);
+            unsent.handler().onComplete(buildResponse(error, 0));
+
+            assertFalse(streamsRebalanceData.topologyPushRequired(),
+                "Flag should be cleared after " + error);
+        }
+    }
+
+    /**
+     * Test the situation when the push fails with a network exception: the
+     * topologyPushRequired flag must remain set so the next poll retries, and 
a failed attempt
+     * is recorded so retry backoff applies.
+     */
+    @Test
+    public void testNetworkExceptionLeavesFlagSetWithBackoff() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        final NetworkClientDelegate.UnsentRequest unsent =
+            manager.poll(time.milliseconds()).unsentRequests.get(0);
+        unsent.handler().onFailure(time.milliseconds(), 
Errors.NETWORK_EXCEPTION.exception());
+
+        assertTrue(streamsRebalanceData.topologyPushRequired());
+        assertEquals(0, 
manager.poll(time.milliseconds()).unsentRequests.size());
+
+        time.sleep(RETRY_BACKOFF_MAX_MS);
+        assertEquals(1, 
manager.poll(time.milliseconds()).unsentRequests.size());
+    }
+
+    /**
+     * Test the situation when the response carries a non-zero ThrottleTimeMs: 
subsequent push
+     * attempts should be delayed by that amount before resuming.
+     */
+    @Test
+    public void testThrottleDelayBlocksSubsequentRequest() {
+        streamsRebalanceData.setWireTopologyDescription(
+            new 
StreamsGroupTopologyDescriptionUpdateRequestData.TopologyDescription());
+        streamsRebalanceData.setTopologyPushRequired(true);
+        
when(coordinatorRequestManager.coordinator()).thenReturn(Optional.of(coordinatorNode));
+
+        final NetworkClientDelegate.UnsentRequest unsent =
+            manager.poll(time.milliseconds()).unsentRequests.get(0);
+        unsent.handler().onComplete(buildResponse(Errors.NOT_COORDINATOR, 
500));
+
+        streamsRebalanceData.setTopologyPushRequired(true);
+        assertEquals(0, 
manager.poll(time.milliseconds()).unsentRequests.size());
+
+        time.sleep(501);
+        assertEquals(1, 
manager.poll(time.milliseconds()).unsentRequests.size());
+    }
+
+    private ClientResponse buildResponse(final Errors error, final int 
throttleTimeMs) {
+        return new ClientResponse(
+            new 
RequestHeader(ApiKeys.STREAMS_GROUP_TOPOLOGY_DESCRIPTION_UPDATE, (short) 0, "", 
1),
+            null,
+            "-1",
+            time.milliseconds(),
+            time.milliseconds(),
+            false,
+            null,
+            null,
+            new StreamsGroupTopologyDescriptionUpdateResponse(
+                new StreamsGroupTopologyDescriptionUpdateResponseData()
+                    .setErrorCode(error.code())
+                    .setThrottleTimeMs(throttleTimeMs)
+            )
+        );
+    }
+}
diff --git 
a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/events/ApplicationEventProcessorTest.java
 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/events/ApplicationEventProcessorTest.java
index 60403008a8b..1f56728ae11 100644
--- 
a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/events/ApplicationEventProcessorTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/events/ApplicationEventProcessorTest.java
@@ -35,6 +35,7 @@ import 
org.apache.kafka.clients.consumer.internals.ShareConsumeRequestManager;
 import 
org.apache.kafka.clients.consumer.internals.ShareHeartbeatRequestManager;
 import org.apache.kafka.clients.consumer.internals.ShareMembershipManager;
 import 
org.apache.kafka.clients.consumer.internals.StreamsGroupHeartbeatRequestManager;
+import 
org.apache.kafka.clients.consumer.internals.StreamsGroupTopologyDescriptionRequestManager;
 import org.apache.kafka.clients.consumer.internals.StreamsMembershipManager;
 import org.apache.kafka.clients.consumer.internals.SubscriptionState;
 import org.apache.kafka.clients.consumer.internals.TopicMetadataRequestManager;
@@ -97,6 +98,7 @@ public class ApplicationEventProcessorTest {
     private SubscriptionState subscriptionState = 
mock(SubscriptionState.class);
     private final ConsumerMetadata metadata = mock(ConsumerMetadata.class);
     private final StreamsGroupHeartbeatRequestManager 
streamsGroupHeartbeatRequestManager = 
mock(StreamsGroupHeartbeatRequestManager.class);
+    private final StreamsGroupTopologyDescriptionRequestManager 
streamsGroupTopologyDescriptionRequestManager = 
mock(StreamsGroupTopologyDescriptionRequestManager.class);
     private final StreamsMembershipManager streamsMembershipManager = 
mock(StreamsMembershipManager.class);
     private final ShareHeartbeatRequestManager shareHeartbeatRequestManager = 
mock(ShareHeartbeatRequestManager.class);
     private final ShareMembershipManager shareMembershipManager = 
mock(ShareMembershipManager.class);
@@ -113,6 +115,7 @@ public class ApplicationEventProcessorTest {
                 withGroupId ? Optional.of(heartbeatRequestManager) : 
Optional.empty(),
                 withGroupId ? Optional.of(membershipManager) : 
Optional.empty(),
                 Optional.empty(),
+                Optional.empty(),
                 Optional.empty()
         );
         processor = new ApplicationEventProcessor(
@@ -134,6 +137,7 @@ public class ApplicationEventProcessorTest {
             withGroupId ? Optional.of(heartbeatRequestManager) : 
Optional.empty(),
             Optional.empty(),
             withGroupId ? Optional.of(streamsGroupHeartbeatRequestManager) : 
Optional.empty(),
+            withGroupId ? 
Optional.of(streamsGroupTopologyDescriptionRequestManager) : Optional.empty(),
             withGroupId ? Optional.of(streamsMembershipManager) : 
Optional.empty()
         );
         processor = new ApplicationEventProcessor(
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
 
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
index 9084e7f8d5f..bb85da626b0 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
@@ -700,7 +700,8 @@ public class StreamThread extends Thread implements 
ProcessingThread {
         return Optional.of(rackId);
     }
 
-    private static StreamsRebalanceData initStreamsRebalanceData(final UUID 
processId,
+    // visible for testing
+    static StreamsRebalanceData initStreamsRebalanceData(final UUID processId,
                                                                  final 
StreamsConfig config,
                                                                  final 
Optional<StreamsRebalanceData.HostInfo> endpoint,
                                                                  final 
Optional<String> rackId,
@@ -1043,7 +1044,7 @@ public class StreamThread extends Thread implements 
ProcessingThread {
         return true;
     }
 
-    // visible for testing
+    // VisibleForTesting
     void maybeGetClientInstanceIds() {
         // we pass in a timeout of zero into each `clientInstanceId()` call
         // to just trigger the "get instance id" background RPC;
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
index a6db72daf56..2c0223fc420 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
@@ -4514,6 +4514,52 @@ public class StreamThreadTest {
         );
     }
 
+    @Test
+    public void shouldPopulateWireTopologyDescriptionWhenPushEnabled() {
+        internalTopologyBuilder.addSource(null, "source1", null, null, null, 
"input-topic");
+        internalTopologyBuilder.addSink("sink1", "output-topic", null, null, 
null, "source1");
+
+        final StreamsConfig config = new StreamsConfig(configProps(false, 
false));
+        final TopologyMetadata topologyMetadata = new 
TopologyMetadata(internalTopologyBuilder, config);
+
+        final StreamsRebalanceData rebalanceData = 
StreamThread.initStreamsRebalanceData(
+            UUID.randomUUID(),
+            config,
+            Optional.empty(),
+            Optional.empty(),
+            topologyMetadata,
+            Map::of,
+            Map::of
+        );
+
+        assertNotNull(rebalanceData.wireTopologyDescription());
+        
assertFalse(rebalanceData.wireTopologyDescription().subtopologies().isEmpty());
+    }
+
+    @Test
+    public void shouldNotPopulateWireTopologyDescriptionWhenPushDisabled() {
+        internalTopologyBuilder.addSource(null, "source1", null, null, null, 
"input-topic");
+        internalTopologyBuilder.addSink("sink1", "output-topic", null, null, 
null, "source1");
+
+        final Properties props = configProps(false, false);
+        
props.setProperty(StreamsConfig.TOPOLOGY_DESCRIPTION_PUSH_ENABLED_CONFIG, 
"false");
+        final StreamsConfig config = new StreamsConfig(props);
+        final TopologyMetadata topologyMetadata = new 
TopologyMetadata(internalTopologyBuilder, config);
+
+        final StreamsRebalanceData rebalanceData = 
StreamThread.initStreamsRebalanceData(
+            UUID.randomUUID(),
+            config,
+            Optional.empty(),
+            Optional.empty(),
+            topologyMetadata,
+            Map::of,
+            Map::of
+        );
+
+        assertNull(rebalanceData.wireTopologyDescription());
+    }
+
+
     private StreamThread setUpThread(final Properties streamsConfigProps) {
         final StreamsConfig config = new StreamsConfig(streamsConfigProps);
         final ConsumerGroupMetadata consumerGroupMetadata = 
Mockito.mock(ConsumerGroupMetadata.class);

Reply via email to