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 d9bb5075 [hotfix][runtime] Aggregate close failures across the 
Exception/Error boundary (#974)
d9bb5075 is described below

commit d9bb5075c909fb188054cc8ee5e2674efbf31b5a
Author: Weiqing Yang <[email protected]>
AuthorDate: Sat Aug 8 22:17:20 2026 -0700

    [hotfix][runtime] Aggregate close failures across the Exception/Error 
boundary (#974)
    
    Generated-by: Claude Code (claude-opus-5)
---
 .../runtime/actionstate/FlussActionStateStore.java |  22 +++-
 .../runtime/actionstate/KafkaActionStateStore.java |  20 ++--
 .../actionstate/FlussActionStateStoreTest.java     | 115 +++++++++++++++++++++
 .../actionstate/KafkaActionStateStoreTest.java     |  57 ++++++++++
 4 files changed, 200 insertions(+), 14 deletions(-)

diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStore.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStore.java
index b6993281..0a20fe2b 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStore.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStore.java
@@ -21,6 +21,7 @@ import org.apache.flink.agents.api.Event;
 import org.apache.flink.agents.plan.AgentConfiguration;
 import org.apache.flink.agents.plan.actions.Action;
 import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.util.ExceptionUtils;
 import org.apache.flink.util.Preconditions;
 import org.apache.fluss.client.Connection;
 import org.apache.fluss.client.ConnectionFactory;
@@ -490,15 +491,28 @@ public class FlussActionStateStore implements 
ActionStateStore {
 
     @Override
     public void close() throws Exception {
-        try {
-            if (table != null) {
+        // Catching Throwable rather than Exception is what keeps the 
connection close reachable
+        // when the table close fails with an Error, and what keeps that first 
failure the one the
+        // caller sees — a later connection failure rides along as suppressed 
instead of replacing
+        // it.
+        Throwable firstException = null;
+        if (table != null) {
+            try {
                 table.close();
+            } catch (Throwable t) {
+                firstException = t;
             }
-        } finally {
-            if (connection != null) {
+        }
+        if (connection != null) {
+            try {
                 connection.close();
+            } catch (Throwable t) {
+                firstException = ExceptionUtils.firstOrSuppressed(t, 
firstException);
             }
         }
+        if (firstException != null) {
+            ExceptionUtils.rethrowException(firstException);
+        }
     }
 
     private void maybeCreateDatabaseAndTable() {
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 ea7ea146..99519acb 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
@@ -22,6 +22,7 @@ import org.apache.flink.agents.api.Event;
 import org.apache.flink.agents.plan.AgentConfiguration;
 import org.apache.flink.agents.plan.actions.Action;
 import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.util.ExceptionUtils;
 import org.apache.kafka.clients.admin.AdminClient;
 import org.apache.kafka.clients.admin.ListTopicsResult;
 import org.apache.kafka.clients.admin.NewTopic;
@@ -331,27 +332,26 @@ public class KafkaActionStateStore implements 
ActionStateStore {
 
     @Override
     public void close() throws Exception {
-        Exception firstException = null;
+        // Catching Throwable rather than Exception is what keeps the consumer 
close reachable when
+        // the producer close fails with an Error, and what keeps that first 
failure the one the
+        // caller sees — a later consumer failure rides along as suppressed 
instead of replacing it.
+        Throwable firstException = null;
         if (producer != null) {
             try {
                 producer.close();
-            } catch (Exception e) {
-                firstException = e;
+            } catch (Throwable t) {
+                firstException = t;
             }
         }
         if (consumer != null) {
             try {
                 consumer.close();
-            } catch (Exception e) {
-                if (firstException == null) {
-                    firstException = e;
-                } else {
-                    firstException.addSuppressed(e);
-                }
+            } catch (Throwable t) {
+                firstException = ExceptionUtils.firstOrSuppressed(t, 
firstException);
             }
         }
         if (firstException != null) {
-            throw firstException;
+            ExceptionUtils.rethrowException(firstException);
         }
     }
 
diff --git 
a/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStoreTest.java
 
b/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStoreTest.java
index 308ad3d4..f6ba5fcc 100644
--- 
a/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStoreTest.java
+++ 
b/runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStoreTest.java
@@ -36,7 +36,9 @@ import java.util.concurrent.CompletableFuture;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.catchThrowable;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -157,6 +159,7 @@ public class FlussActionStateStoreTest {
         assertThat(actionStates).isEmpty();
     }
 
+    /** Contract: both the table and the connection are closed when neither 
close fails. */
     @Test
     void testCloseClosesResources() throws Exception {
         Table mockTable = mock(Table.class);
@@ -170,4 +173,116 @@ public class FlussActionStateStoreTest {
         verify(mockTable).close();
         verify(mockConnection).close();
     }
+
+    /**
+     * Contract: a recorded table-close failure survives a clean connection 
close — the table's
+     * exception still reaches the caller, and the connection is closed.
+     */
+    @Test
+    void testCloseClosesConnectionWhenTableCloseFails() throws Exception {
+        Table failingTable = mock(Table.class);
+        Connection mockConnection = mock(Connection.class);
+        RuntimeException tableFailure = new RuntimeException("table close 
failed");
+        doThrow(tableFailure).when(failingTable).close();
+
+        FlussActionStateStore closeableStore =
+                new FlussActionStateStore(actionStates, mockConnection, 
failingTable, mockWriter);
+
+        
assertThat(catchThrowable(closeableStore::close)).isSameAs(tableFailure);
+
+        verify(mockConnection).close();
+    }
+
+    /**
+     * Contract: when both closes fail, the table's exception is the one 
thrown and the connection's
+     * is attached to it as a suppressed exception, so neither failure is lost.
+     */
+    @Test
+    void testCloseKeepsTableFailureWhenBothCloseFail() throws Exception {
+        Table failingTable = mock(Table.class);
+        Connection failingConnection = mock(Connection.class);
+        IOException tableFailure = new IOException("table close failed");
+        IOException connectionFailure = new IOException("connection close 
failed");
+        doThrow(tableFailure).when(failingTable).close();
+        doThrow(connectionFailure).when(failingConnection).close();
+
+        FlussActionStateStore closeableStore =
+                new FlussActionStateStore(
+                        actionStates, failingConnection, failingTable, 
mockWriter);
+
+        Throwable thrown = catchThrowable(closeableStore::close);
+
+        assertThat(thrown).isSameAs(tableFailure);
+        assertThat(thrown.getSuppressed()).containsExactly(connectionFailure);
+    }
+
+    /**
+     * Contract: when only the connection close fails, its exception reaches 
the caller unchanged,
+     * with nothing attached as suppressed.
+     */
+    @Test
+    void testCloseThrowsConnectionFailureWhenOnlyConnectionCloseFails() throws 
Exception {
+        Table mockTable = mock(Table.class);
+        Connection failingConnection = mock(Connection.class);
+        RuntimeException connectionFailure = new RuntimeException("connection 
close failed");
+        doThrow(connectionFailure).when(failingConnection).close();
+
+        FlussActionStateStore closeableStore =
+                new FlussActionStateStore(actionStates, failingConnection, 
mockTable, mockWriter);
+
+        Throwable thrown = catchThrowable(closeableStore::close);
+
+        assertThat(thrown).isSameAs(connectionFailure);
+        assertThat(thrown.getSuppressed()).isEmpty();
+    }
+
+    /**
+     * Contract: when closing the table throws a non-{@code Exception} {@code 
Throwable} and the
+     * connection close then fails too, the throwable stays the failure the 
caller sees, the
+     * connection is still closed, and the connection's failure is attached as 
suppressed.
+     */
+    @Test
+    void testCloseKeepsTableErrorWhenConnectionCloseAlsoFails() throws 
Exception {
+        Table failingTable = mock(Table.class);
+        Connection failingConnection = mock(Connection.class);
+        NoClassDefFoundError tableFailure = new 
NoClassDefFoundError("simulated teardown failure");
+        RuntimeException connectionFailure = new RuntimeException("connection 
close failed");
+        doThrow(tableFailure).when(failingTable).close();
+        doThrow(connectionFailure).when(failingConnection).close();
+
+        FlussActionStateStore closeableStore =
+                new FlussActionStateStore(
+                        actionStates, failingConnection, failingTable, 
mockWriter);
+
+        Throwable thrown = catchThrowable(closeableStore::close);
+
+        assertThat(thrown).isSameAs(tableFailure);
+        assertThat(thrown.getSuppressed()).containsExactly(connectionFailure);
+        verify(failingConnection).close();
+    }
+
+    /**
+     * Contract: when the table close fails and the connection close then 
throws a non-{@code
+     * Exception} {@code Throwable}, the table's exception stays the failure 
the caller sees and the
+     * connection's throwable is attached as suppressed.
+     */
+    @Test
+    void testCloseKeepsTableFailureWhenConnectionCloseThrowsError() throws 
Exception {
+        Table failingTable = mock(Table.class);
+        Connection failingConnection = mock(Connection.class);
+        IOException tableFailure = new IOException("table close failed");
+        NoClassDefFoundError connectionFailure =
+                new NoClassDefFoundError("simulated teardown failure");
+        doThrow(tableFailure).when(failingTable).close();
+        doThrow(connectionFailure).when(failingConnection).close();
+
+        FlussActionStateStore closeableStore =
+                new FlussActionStateStore(
+                        actionStates, failingConnection, failingTable, 
mockWriter);
+
+        Throwable thrown = catchThrowable(closeableStore::close);
+
+        assertThat(thrown).isSameAs(tableFailure);
+        assertThat(thrown.getSuppressed()).containsExactly(connectionFailure);
+    }
 }
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 c0a01bd0..1d8ae231 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
@@ -39,6 +39,7 @@ 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.assertj.core.api.Assertions.catchThrowable;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
@@ -335,6 +336,62 @@ public class KafkaActionStateStoreTest {
         assertThat(thrown.getSuppressed()).isEmpty();
     }
 
+    /**
+     * Contract: when closing the producer throws a non-{@code Exception} 
{@code Throwable}, the
+     * consumer is still closed and the throwable reaches the caller unchanged.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    void testCloseClosesConsumerWhenProducerCloseThrowsError() {
+        Producer<String, ActionState> failingProducer = mock(Producer.class);
+        Consumer<String, ActionState> consumer = mock(Consumer.class);
+        NoClassDefFoundError producerFailure =
+                new NoClassDefFoundError("simulated teardown failure");
+        doThrow(producerFailure).when(failingProducer).close();
+
+        KafkaActionStateStore store =
+                new KafkaActionStateStore(
+                        actionStates,
+                        new AgentConfiguration(),
+                        failingProducer,
+                        consumer,
+                        TEST_TOPIC);
+
+        assertThat(catchThrowable(store::close)).isSameAs(producerFailure);
+
+        verify(consumer).close();
+    }
+
+    /**
+     * Contract: when the producer close fails and the consumer close then 
throws a non-{@code
+     * Exception} {@code Throwable}, the producer's exception stays the 
failure the caller sees and
+     * the consumer's throwable is attached as suppressed.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    void testCloseKeepsProducerFailureWhenConsumerCloseThrowsError() {
+        Producer<String, ActionState> failingProducer = mock(Producer.class);
+        Consumer<String, ActionState> failingConsumer = mock(Consumer.class);
+        RuntimeException producerFailure = new RuntimeException("producer 
close failed");
+        NoClassDefFoundError consumerFailure =
+                new NoClassDefFoundError("simulated teardown failure");
+        doThrow(producerFailure).when(failingProducer).close();
+        doThrow(consumerFailure).when(failingConsumer).close();
+
+        KafkaActionStateStore store =
+                new KafkaActionStateStore(
+                        actionStates,
+                        new AgentConfiguration(),
+                        failingProducer,
+                        failingConsumer,
+                        TEST_TOPIC);
+
+        Throwable thrown = catchThrowable(store::close);
+
+        assertThat(thrown).isSameAs(producerFailure);
+        assertThat(thrown.getSuppressed()).containsExactly(consumerFailure);
+    }
+
     /** Contract: both the producer and the consumer are closed when neither 
close fails. */
     @Test
     void testCloseClosesProducerAndConsumer() throws Exception {

Reply via email to