This is an automated email from the ASF dual-hosted git repository.
lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-java.git
The following commit(s) were added to refs/heads/main by this push:
new 2329174e0 GH-863: [JDBC] Suppress benign exceptions from gRPC layer on
ArrowFlightSqlClientHandler#close (#910)
2329174e0 is described below
commit 2329174e0bf35733b5ac99b7e36465a601ae20e8
Author: Pedro Matias <[email protected]>
AuthorDate: Thu Dec 4 09:33:49 2025 +0000
GH-863: [JDBC] Suppress benign exceptions from gRPC layer on
ArrowFlightSqlClientHandler#close (#910)
## What's Changed
When using the Flight SQL JDBC driver with connection pooling and a
catalog parameter, ArrowFlightSqlClientHandler.close() performs a
CloseSession RPC that can fail during gRPC channel shutdown.
These transient failures (UNAVAILABLE or INTERNAL with "Connection
closed after GOAWAY") cause noisy errors in pooling frameworks like
Apache Commons DBCP.
With this PR these exceptions will instead be suppressed and logged,
following the procedure that was used for
[ARROW-17785](https://issues.apache.org/jira/browse/ARROW-17785)
### Are these changes tested?
Yes
Closes #863
---------
Co-authored-by: Vando Pereira <[email protected]>
---
.../jdbc/client/ArrowFlightSqlClientHandler.java | 81 +++++++++++++++++---
.../client/ArrowFlightSqlClientHandlerTest.java | 88 ++++++++++++++++++++++
2 files changed, 160 insertions(+), 9 deletions(-)
diff --git
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java
index a3f690037..5dc7e0e2e 100644
---
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java
+++
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java
@@ -262,15 +262,85 @@ public final class ArrowFlightSqlClientHandler implements
AutoCloseable {
@Override
public void close() throws SQLException {
if (catalog.isPresent()) {
- sqlClient.closeSession(new CloseSessionRequest(), getOptions());
+ try {
+ sqlClient.closeSession(new CloseSessionRequest(), getOptions());
+ } catch (FlightRuntimeException fre) {
+ handleBenignCloseException(
+ fre, "Failed to close Flight SQL session.", "closing Flight SQL
session");
+ }
}
try {
AutoCloseables.close(sqlClient);
+ } catch (FlightRuntimeException fre) {
+ handleBenignCloseException(
+ fre, "Failed to clean up client resources.", "closing Flight SQL
client");
} catch (final Exception e) {
throw new SQLException("Failed to clean up client resources.", e);
}
}
+ /**
+ * Handles FlightRuntimeException during close operations, suppressing
benign gRPC shutdown errors
+ * while re-throwing genuine failures.
+ *
+ * @param fre the FlightRuntimeException to handle
+ * @param sqlErrorMessage the SQLException message to use for genuine
failures
+ * @param operationDescription description of the operation for logging
+ * @throws SQLException if the exception represents a genuine failure
+ */
+ private void handleBenignCloseException(
+ FlightRuntimeException fre, String sqlErrorMessage, String
operationDescription)
+ throws SQLException {
+ if (isBenignCloseException(fre)) {
+ logSuppressedCloseException(fre, operationDescription);
+ } else {
+ throw new SQLException(sqlErrorMessage, fre);
+ }
+ }
+
+ /**
+ * Handles FlightRuntimeException during close operations, suppressing
benign gRPC shutdown errors
+ * while re-throwing genuine failures as FlightRuntimeException.
+ *
+ * @param fre the FlightRuntimeException to handle
+ * @param operationDescription description of the operation for logging
+ * @throws FlightRuntimeException if the exception represents a genuine
failure
+ */
+ private void handleBenignCloseException(FlightRuntimeException fre, String
operationDescription)
+ throws FlightRuntimeException {
+ if (isBenignCloseException(fre)) {
+ logSuppressedCloseException(fre, operationDescription);
+ } else {
+ throw fre;
+ }
+ }
+
+ /**
+ * Determines if a FlightRuntimeException represents a benign close
operation error that should be
+ * suppressed.
+ *
+ * @param fre the FlightRuntimeException to check
+ * @return true if the exception should be suppressed, false otherwise
+ */
+ private boolean isBenignCloseException(FlightRuntimeException fre) {
+ return fre.status().code().equals(FlightStatusCode.UNAVAILABLE)
+ || (fre.status().code().equals(FlightStatusCode.INTERNAL)
+ && fre.getMessage() != null
+ && fre.getMessage().contains("Connection closed after GOAWAY"));
+ }
+
+ /**
+ * Logs a suppressed close exception with appropriate level based on debug
settings.
+ *
+ * @param fre the FlightRuntimeException being suppressed
+ * @param operationDescription description of the operation for logging
+ */
+ private void logSuppressedCloseException(
+ FlightRuntimeException fre, String operationDescription) {
+ // ARROW-17785 and GH-863: suppress exceptions caused by flaky gRPC layer
during shutdown
+ LOGGER.debug("Suppressed error {}", operationDescription, fre);
+ }
+
/** A prepared statement handler. */
public interface PreparedStatement extends AutoCloseable {
/**
@@ -386,14 +456,7 @@ public final class ArrowFlightSqlClientHandler implements
AutoCloseable {
try {
preparedStatement.close(getOptions());
} catch (FlightRuntimeException fre) {
- // ARROW-17785: suppress exceptions caused by flaky gRPC layer
- if (fre.status().code().equals(FlightStatusCode.UNAVAILABLE)
- || (fre.status().code().equals(FlightStatusCode.INTERNAL)
- && fre.getMessage().contains("Connection closed after
GOAWAY"))) {
- LOGGER.warn("Supressed error closing PreparedStatement", fre);
- return;
- }
- throw fre;
+ handleBenignCloseException(fre, "closing PreparedStatement");
}
}
};
diff --git
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerTest.java
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerTest.java
new file mode 100644
index 000000000..d5973ab5d
--- /dev/null
+++
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.arrow.driver.jdbc.client;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+import org.apache.arrow.flight.CallOption;
+import org.apache.arrow.flight.CallStatus;
+import org.apache.arrow.flight.CloseSessionRequest;
+import org.apache.arrow.flight.FlightStatusCode;
+import org.apache.arrow.flight.sql.FlightSqlClient;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class ArrowFlightSqlClientHandlerTest {
+
+ @ParameterizedTest
+ @MethodSource
+ public void testCloseHandlesFlightRuntimeException(
+ boolean throwFromCloseSession, CallStatus callStatus, boolean
shouldSuppress)
+ throws Exception {
+ FlightSqlClient sqlClient = mock(FlightSqlClient.class);
+ String cacheKey = "cacheKey";
+ Optional<String> catalog =
+ throwFromCloseSession ? Optional.of("test_catalog") : Optional.empty();
+ final Collection<CallOption> credentialOptions = new ArrayList<>();
+ ArrowFlightSqlClientHandler.Builder builder = new
ArrowFlightSqlClientHandler.Builder();
+
+ if (throwFromCloseSession) {
+ doThrow(callStatus.toRuntimeException())
+ .when(sqlClient)
+ .closeSession(any(CloseSessionRequest.class),
any(CallOption[].class));
+ } else {
+ doThrow(callStatus.toRuntimeException()).when(sqlClient).close();
+ }
+
+ ArrowFlightSqlClientHandler sqlClientHandler =
+ new ArrowFlightSqlClientHandler(
+ cacheKey, sqlClient, builder, credentialOptions, catalog, null);
+
+ if (shouldSuppress) {
+ assertDoesNotThrow(sqlClientHandler::close);
+ } else {
+ assertThrows(SQLException.class, sqlClientHandler::close);
+ }
+ }
+
+ private static Object[] testCloseHandlesFlightRuntimeException() {
+ CallStatus benignInternalError =
+ new CallStatus(FlightStatusCode.INTERNAL, null, "Connection closed
after GOAWAY", null);
+ CallStatus notBenignInternalError =
+ new CallStatus(FlightStatusCode.INTERNAL, null, "Not a benign internal
error", null);
+ CallStatus unavailableError = new CallStatus(FlightStatusCode.UNAVAILABLE,
null, null, null);
+ CallStatus unknownError = new CallStatus(FlightStatusCode.UNKNOWN, null,
null, null);
+ return new Object[] {
+ new Object[] {true, benignInternalError, true},
+ new Object[] {false, benignInternalError, true},
+ new Object[] {true, notBenignInternalError, false},
+ new Object[] {false, notBenignInternalError, false},
+ new Object[] {true, unavailableError, true},
+ new Object[] {false, unavailableError, true},
+ new Object[] {true, unknownError, false},
+ new Object[] {false, unknownError, false},
+ };
+ }
+}