Skip to content

Commit a1042f8

Browse files
authored
perf: optimize isValid implementation (googleapis#1444)
Cloud Spanner JDBC connections do not maintain a physical connection to Cloud Spanner, but are merely a wrapper around the underlying Java client library. This again uses a pool of gRPC channels to communicate with Cloud Spanner. This means that a single JDBC connection will never lose its network connection with Cloud Spanner, and checking whether it is valid or not by executing a query every time is not useful. Instead, the check should: 1. Verify that a connection has successfully been established with Cloud Spanner. The result should be cached for all JDBC connections using the same Cloud Spanner client library instance. 2. Verify that the connection has not been closed. The above can be achieved by checking that the dialect of the database that the connection is connected to has been successfully fetched. This result is cached in the client library, and being able to get that means that there has been a valid connection. This means that the isValid method will still return true if the network connectivity has been lost completely between the client and Cloud Spanner. However, this check is mostly used by connection pools to determine whether a connection is safe to be handed out to an application, and when all network connectivity has been lost, this will apply to all JDBC connections and not just one, meaning that the check is void. The original isValid check can be enabled by setting the System property spanner.jdbc.use_legacy_is_valid_check to true or setting the Environment variable SPANNER_JDBC_USE_LEGACY_IS_VALID_CHECK to true. Fixes googleapis#1443
1 parent dbab83a commit a1042f8

2 files changed

Lines changed: 56 additions & 12 deletions

File tree

java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.cloud.spanner.connection.ConnectionOptions;
3030
import com.google.cloud.spanner.connection.SavepointSupport;
3131
import com.google.cloud.spanner.connection.TransactionMode;
32+
import com.google.common.base.Strings;
3233
import com.google.common.collect.ImmutableList;
3334
import com.google.common.collect.Iterators;
3435
import java.sql.Array;
@@ -57,14 +58,42 @@ class JdbcConnection extends AbstractJdbcConnection {
5758
"Only result sets with concurrency CONCUR_READ_ONLY are supported";
5859
private static final String ONLY_CLOSE_CURSORS_AT_COMMIT =
5960
"Only result sets with holdability CLOSE_CURSORS_AT_COMMIT are supported";
60-
static final String IS_VALID_QUERY = "SELECT 1";
61+
62+
/**
63+
* This query is used to check the aliveness of the connection if legacy alive check has been
64+
* enabled. As Cloud Spanner JDBC connections do not maintain a physical or logical connection to
65+
* Cloud Spanner, there is also no point in repeatedly executing a simple query to check whether a
66+
* connection is alive. Instead, we rely on the result from the initial query to Spanner that
67+
* determines the dialect to determine whether the connection is alive or not. This result is
68+
* cached for all JDBC connections using the same {@link com.google.cloud.spanner.Spanner}
69+
* instance.
70+
*
71+
* <p>The legacy {@link #isValid(int)} check using a SELECT 1 statement can be enabled by setting
72+
* the System property spanner.jdbc.use_legacy_is_valid_check to true or setting the environment
73+
* variable SPANNER_JDBC_USE_LEGACY_IS_VALID_CHECK to true.
74+
*/
75+
static final String LEGACY_IS_VALID_QUERY = "SELECT 1";
6176

6277
static final ImmutableList<String> NO_GENERATED_KEY_COLUMNS = ImmutableList.of();
6378

6479
private Map<String, Class<?>> typeMap = new HashMap<>();
6580

81+
private final boolean useLegacyIsValidCheck;
82+
6683
JdbcConnection(String connectionUrl, ConnectionOptions options) throws SQLException {
6784
super(connectionUrl, options);
85+
this.useLegacyIsValidCheck = useLegacyValidCheck();
86+
}
87+
88+
static boolean useLegacyValidCheck() {
89+
String value = System.getProperty("spanner.jdbc.use_legacy_is_valid_check");
90+
if (Strings.isNullOrEmpty(value)) {
91+
value = System.getenv("SPANNER_JDBC_USE_LEGACY_IS_VALID_CHECK");
92+
}
93+
if (!Strings.isNullOrEmpty(value)) {
94+
return Boolean.parseBoolean(value);
95+
}
96+
return false;
6897
}
6998

7099
@Override
@@ -347,23 +376,38 @@ public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
347376
this.typeMap = new HashMap<>(map);
348377
}
349378

379+
boolean isUseLegacyIsValidCheck() {
380+
return useLegacyIsValidCheck;
381+
}
382+
350383
@Override
351384
public boolean isValid(int timeout) throws SQLException {
352385
JdbcPreconditions.checkArgument(timeout >= 0, "timeout must be >= 0");
353386
if (!isClosed()) {
387+
if (isUseLegacyIsValidCheck()) {
388+
return legacyIsValid(timeout);
389+
}
354390
try {
355-
Statement statement = createStatement();
356-
statement.setQueryTimeout(timeout);
357-
try (ResultSet rs = statement.executeQuery(IS_VALID_QUERY)) {
358-
if (rs.next()) {
359-
if (rs.getLong(1) == 1L) {
360-
return true;
361-
}
391+
return getDialect() != null;
392+
} catch (Exception ignore) {
393+
// ignore and fall through.
394+
}
395+
}
396+
return false;
397+
}
398+
399+
private boolean legacyIsValid(int timeout) throws SQLException {
400+
try (Statement statement = createStatement()) {
401+
statement.setQueryTimeout(timeout);
402+
try (ResultSet rs = statement.executeQuery(LEGACY_IS_VALID_QUERY)) {
403+
if (rs.next()) {
404+
if (rs.getLong(1) == 1L) {
405+
return true;
362406
}
363407
}
364-
} catch (SQLException e) {
365-
// ignore
366408
}
409+
} catch (SQLException e) {
410+
// ignore and fall through.
367411
}
368412
return false;
369413
}

java-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ public void testIsValid() throws SQLException {
502502
mock(com.google.cloud.spanner.connection.Connection.class);
503503
when(spannerConnection.getDialect()).thenReturn(dialect);
504504
when(options.getConnection()).thenReturn(spannerConnection);
505-
Statement statement = Statement.of(JdbcConnection.IS_VALID_QUERY);
505+
Statement statement = Statement.of(JdbcConnection.LEGACY_IS_VALID_QUERY);
506506

507507
// Verify that an opened connection that returns a result set is valid.
508508
try (JdbcConnection connection = new JdbcConnection("url", options)) {
@@ -517,7 +517,7 @@ public void testIsValid() throws SQLException {
517517
}
518518

519519
// Now let the query return an error. isValid should now return false.
520-
when(spannerConnection.executeQuery(statement))
520+
when(spannerConnection.getDialect())
521521
.thenThrow(
522522
SpannerExceptionFactory.newSpannerException(
523523
ErrorCode.ABORTED, "the current transaction has been aborted"));

0 commit comments

Comments
 (0)