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

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new e5734203 [hotfix][runtime] Close the Kafka consumer when the producer 
fails to close (#948)
e5734203 is described below

commit e5734203721eeacee51f98c6f4e0ef843fb229ba
Author: Weiqing Yang <[email protected]>
AuthorDate: Wed Aug 5 03:51:58 2026 -0700

    [hotfix][runtime] Close the Kafka consumer when the producer fails to close 
(#948)
---
 .../runtime/actionstate/KafkaActionStateStore.java | 20 ++++-
 .../actionstate/KafkaActionStateStoreTest.java     | 89 ++++++++++++++++++++++
 2 files changed, 107 insertions(+), 2 deletions(-)

diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStore.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStore.java
index f09e5cd2..ea7ea146 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStore.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStore.java
@@ -331,11 +331,27 @@ public class KafkaActionStateStore implements 
ActionStateStore {
 
     @Override
     public void close() throws Exception {
+        Exception firstException = null;
         if (producer != null) {
-            producer.close();
+            try {
+                producer.close();
+            } catch (Exception e) {
+                firstException = e;
+            }
         }
         if (consumer != null) {
-            consumer.close();
+            try {
+                consumer.close();
+            } catch (Exception e) {
+                if (firstException == null) {
+                    firstException = e;
+                } else {
+                    firstException.addSuppressed(e);
+                }
+            }
+        }
+        if (firstException != null) {
+            throw firstException;
         }
     }
 
diff --git 
a/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStoreTest.java
 
b/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStoreTest.java
index 7cf829c0..c0a01bd0 100644
--- 
a/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStoreTest.java
+++ 
b/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/KafkaActionStateStoreTest.java
@@ -21,9 +21,11 @@ import org.apache.flink.agents.api.Event;
 import org.apache.flink.agents.api.InputEvent;
 import org.apache.flink.agents.plan.AgentConfiguration;
 import org.apache.flink.agents.plan.actions.Action;
+import org.apache.kafka.clients.consumer.Consumer;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
 import org.apache.kafka.clients.consumer.MockConsumer;
 import org.apache.kafka.clients.producer.MockProducer;
+import org.apache.kafka.clients.producer.Producer;
 import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.common.PartitionInfo;
 import org.apache.kafka.common.TopicPartition;
@@ -38,6 +40,9 @@ import java.util.Map;
 import static 
org.apache.kafka.clients.consumer.internals.AutoOffsetResetStrategy.EARLIEST;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 /** Unit tests for {@link KafkaActionStateStore}. */
 public class KafkaActionStateStoreTest {
@@ -254,4 +259,88 @@ public class KafkaActionStateStoreTest {
                                 ActionStateUtil.generateKey(TEST_KEY, 3L, 
testAction, testEvent)))
                 .isEqualTo(thirdState);
     }
+
+    /** Contract: the consumer is closed even when closing the producer 
throws. */
+    @Test
+    @SuppressWarnings("unchecked")
+    void testCloseClosesConsumerWhenProducerCloseFails() {
+        Producer<String, ActionState> failingProducer = mock(Producer.class);
+        Consumer<String, ActionState> consumer = mock(Consumer.class);
+        doThrow(new RuntimeException("producer close 
failed")).when(failingProducer).close();
+
+        KafkaActionStateStore store =
+                new KafkaActionStateStore(
+                        actionStates,
+                        new AgentConfiguration(),
+                        failingProducer,
+                        consumer,
+                        TEST_TOPIC);
+
+        assertThrows(RuntimeException.class, store::close);
+
+        verify(consumer).close();
+    }
+
+    /**
+     * Contract: when both closes fail, the producer's exception is the one 
thrown and the
+     * consumer's is attached to it as a suppressed exception, so neither 
failure is lost.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    void testCloseKeepsProducerFailureWhenBothCloseFail() {
+        Producer<String, ActionState> failingProducer = mock(Producer.class);
+        Consumer<String, ActionState> failingConsumer = mock(Consumer.class);
+        RuntimeException producerFailure = new RuntimeException("producer 
close failed");
+        RuntimeException consumerFailure = new RuntimeException("consumer 
close failed");
+        doThrow(producerFailure).when(failingProducer).close();
+        doThrow(consumerFailure).when(failingConsumer).close();
+
+        KafkaActionStateStore store =
+                new KafkaActionStateStore(
+                        actionStates,
+                        new AgentConfiguration(),
+                        failingProducer,
+                        failingConsumer,
+                        TEST_TOPIC);
+
+        RuntimeException thrown = assertThrows(RuntimeException.class, 
store::close);
+
+        assertThat(thrown).isSameAs(producerFailure);
+        assertThat(thrown.getSuppressed()).containsExactly(consumerFailure);
+    }
+
+    /**
+     * Contract: when only the consumer close fails, its exception reaches the 
caller unchanged,
+     * with nothing attached as suppressed.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    void testCloseThrowsConsumerFailureWhenOnlyConsumerCloseFails() {
+        Producer<String, ActionState> producer = mock(Producer.class);
+        Consumer<String, ActionState> failingConsumer = mock(Consumer.class);
+        RuntimeException consumerFailure = new RuntimeException("consumer 
close failed");
+        doThrow(consumerFailure).when(failingConsumer).close();
+
+        KafkaActionStateStore store =
+                new KafkaActionStateStore(
+                        actionStates,
+                        new AgentConfiguration(),
+                        producer,
+                        failingConsumer,
+                        TEST_TOPIC);
+
+        RuntimeException thrown = assertThrows(RuntimeException.class, 
store::close);
+
+        assertThat(thrown).isSameAs(consumerFailure);
+        assertThat(thrown.getSuppressed()).isEmpty();
+    }
+
+    /** Contract: both the producer and the consumer are closed when neither 
close fails. */
+    @Test
+    void testCloseClosesProducerAndConsumer() throws Exception {
+        actionStateStore.close();
+
+        assertThat(mockProducer.closed()).isTrue();
+        assertThat(mockConsumer.closed()).isTrue();
+    }
 }

Reply via email to