Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 832064c

Browse files
authored
perf: optimize JdbcDataSource#getConnection() (#2371)
The `JdbcDataSource#getConnection()` method repeatedly executed a number of steps that were not necessary for each new connection. This has now been optimized, so they are only executed once, as long as the properties of the DataSource do not change.
1 parent a4b5c21 commit 832064c

5 files changed

Lines changed: 137 additions & 36 deletions

File tree

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@
260260
<spanner.testenv.instance>
261261
projects/gcloud-devel/instances/spanner-testing-east1
262262
</spanner.testenv.instance>
263+
<java.util.logging.config.file>logging.properties</java.util.logging.config.file>
263264
</systemPropertyVariables>
264265
</configuration>
265266
</plugin>
@@ -274,6 +275,7 @@
274275
<spanner.testenv.instance>
275276
projects/gcloud-devel/instances/spanner-testing-east1
276277
</spanner.testenv.instance>
278+
<java.util.logging.config.file>logging.properties</java.util.logging.config.file>
277279
</systemPropertyVariables>
278280
<forkedProcessTimeoutInSeconds>2400</forkedProcessTimeoutInSeconds>
279281
<forkCount>4</forkCount>

src/main/java/com/google/cloud/spanner/jdbc/JdbcDataSource.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package com.google.cloud.spanner.jdbc;
1818

19+
import static com.google.cloud.spanner.jdbc.JdbcDriver.appendPropertiesToUrl;
20+
import static com.google.cloud.spanner.jdbc.JdbcDriver.buildConnectionOptions;
21+
import static com.google.cloud.spanner.jdbc.JdbcDriver.maybeAddUserAgent;
22+
1923
import com.google.cloud.spanner.connection.ConnectionOptions;
2024
import com.google.rpc.Code;
2125
import java.io.PrintWriter;
2226
import java.sql.Connection;
23-
import java.sql.DriverManager;
2427
import java.sql.SQLException;
2528
import java.sql.SQLFeatureNotSupportedException;
2629
import java.util.Properties;
@@ -35,6 +38,8 @@ public class JdbcDataSource extends AbstractJdbcWrapper implements DataSource {
3538
private Boolean readonly;
3639
private Boolean retryAbortsInternally;
3740

41+
private volatile ConnectionOptions cachedConnectionOptions;
42+
3843
// Make sure the JDBC driver class is loaded.
3944
static {
4045
try {
@@ -76,12 +81,22 @@ public Connection getConnection() throws SQLException {
7681
throw JdbcSqlExceptionFactory.of(
7782
"There is no URL specified for this data source", Code.FAILED_PRECONDITION);
7883
}
79-
if (!JdbcDriver.getRegisteredDriver().acceptsURL(getUrl())) {
80-
throw JdbcSqlExceptionFactory.of(
81-
"The URL " + getUrl() + " is not valid for the data source " + getClass().getName(),
82-
Code.FAILED_PRECONDITION);
84+
if (cachedConnectionOptions == null) {
85+
synchronized (this) {
86+
if (cachedConnectionOptions == null) {
87+
if (!JdbcDriver.getRegisteredDriver().acceptsURL(getUrl())) {
88+
throw JdbcSqlExceptionFactory.of(
89+
"The URL " + getUrl() + " is not valid for the data source " + getClass().getName(),
90+
Code.FAILED_PRECONDITION);
91+
}
92+
Properties properties = createProperties();
93+
maybeAddUserAgent(properties);
94+
String connectionUri = appendPropertiesToUrl(url.substring(5), properties);
95+
cachedConnectionOptions = buildConnectionOptions(connectionUri, properties);
96+
}
97+
}
8398
}
84-
return DriverManager.getConnection(getUrl(), createProperties());
99+
return new JdbcConnection(getUrl(), cachedConnectionOptions);
85100
}
86101

87102
@Override
@@ -114,6 +129,12 @@ public boolean isClosed() {
114129
return false;
115130
}
116131

132+
private void clearCachedConnectionOptions() {
133+
synchronized (this) {
134+
cachedConnectionOptions = null;
135+
}
136+
}
137+
117138
/**
118139
* @return the JDBC URL to use for this {@link DataSource}.
119140
*/
@@ -125,6 +146,7 @@ public String getUrl() {
125146
* @param url The JDBC URL to use for this {@link DataSource}.
126147
*/
127148
public void setUrl(String url) {
149+
clearCachedConnectionOptions();
128150
this.url = url;
129151
}
130152

@@ -143,6 +165,7 @@ public String getCredentials() {
143165
* connection URL will be used.
144166
*/
145167
public void setCredentials(String credentials) {
168+
clearCachedConnectionOptions();
146169
this.credentials = credentials;
147170
}
148171

@@ -161,6 +184,7 @@ public Boolean getAutocommit() {
161184
* the connection URL will be used.
162185
*/
163186
public void setAutocommit(Boolean autocommit) {
187+
clearCachedConnectionOptions();
164188
this.autocommit = autocommit;
165189
}
166190

@@ -179,6 +203,7 @@ public Boolean getReadonly() {
179203
* URL will be used.
180204
*/
181205
public void setReadonly(Boolean readonly) {
206+
clearCachedConnectionOptions();
182207
this.readonly = readonly;
183208
}
184209

@@ -197,6 +222,7 @@ public Boolean getRetryAbortsInternally() {
197222
* this property, the value in the connection URL will be used.
198223
*/
199224
public void setRetryAbortsInternally(Boolean retryAbortsInternally) {
225+
clearCachedConnectionOptions();
200226
this.retryAbortsInternally = retryAbortsInternally;
201227
}
202228
}

src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.sql.SQLWarning;
3838
import java.util.Map.Entry;
3939
import java.util.Properties;
40+
import java.util.function.Supplier;
4041
import java.util.logging.Logger;
4142
import java.util.regex.Matcher;
4243
import java.util.regex.Pattern;
@@ -247,7 +248,7 @@ public Connection connect(String url, Properties info) throws SQLException {
247248
return null;
248249
}
249250

250-
private ConnectionOptions buildConnectionOptions(String connectionUrl, Properties info) {
251+
static ConnectionOptions buildConnectionOptions(String connectionUrl, Properties info) {
251252
ConnectionOptions.Builder builder =
252253
ConnectionOptions.newBuilder().setTracingPrefix("JDBC").setUri(connectionUrl);
253254
if (info.containsKey(OPEN_TELEMETRY_PROPERTY_KEY)
@@ -272,40 +273,42 @@ static void maybeAddUserAgent(Properties properties) {
272273
}
273274
}
274275

275-
static boolean isHibernate() {
276-
// Cache the result as the check is relatively expensive, and we also don't want to create
277-
// multiple different Spanner instances just to get the correct user-agent in every case.
278-
return Suppliers.memoize(
279-
() -> {
280-
try {
281-
// First check if the Spanner Hibernate dialect is on the classpath. If it is, then
282-
// we assume that Hibernate will (eventually) be used.
283-
Class.forName(
284-
"com.google.cloud.spanner.hibernate.SpannerDialect",
285-
/* initialize= */ false,
286-
JdbcDriver.class.getClassLoader());
287-
return true;
288-
} catch (Throwable ignore) {
289-
}
276+
private static final Supplier<Boolean> isHibernate =
277+
Suppliers.memoize(
278+
() -> {
279+
try {
280+
// First check if the Spanner Hibernate dialect is on the classpath. If it is, then
281+
// we assume that Hibernate will (eventually) be used.
282+
Class.forName(
283+
"com.google.cloud.spanner.hibernate.SpannerDialect",
284+
/* initialize= */ false,
285+
JdbcDriver.class.getClassLoader());
286+
return true;
287+
} catch (Throwable ignore) {
288+
}
290289

291-
// If we did not find the Spanner Hibernate dialect on the classpath, then do a
292-
// check if the connection is still being created by Hibernate using the built-in
293-
// Spanner dialect in Hibernate.
294-
try {
295-
StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
296-
for (StackTraceElement element : callStack) {
297-
if (element.getClassName().contains(".hibernate.")) {
298-
return true;
299-
}
290+
// If we did not find the Spanner Hibernate dialect on the classpath, then do a
291+
// check if the connection is still being created by Hibernate using the built-in
292+
// Spanner dialect in Hibernate.
293+
try {
294+
StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
295+
for (StackTraceElement element : callStack) {
296+
if (element.getClassName().contains(".hibernate.")) {
297+
return true;
300298
}
301-
} catch (Throwable ignore) {
302299
}
303-
return false;
304-
})
305-
.get();
300+
} catch (Throwable ignore) {
301+
}
302+
return false;
303+
});
304+
305+
static boolean isHibernate() {
306+
// Cache the result as the check is relatively expensive, and we also don't want to create
307+
// multiple different Spanner instances just to get the correct user-agent in every case.
308+
return isHibernate.get();
306309
}
307310

308-
private String appendPropertiesToUrl(String url, Properties info) {
311+
static String appendPropertiesToUrl(String url, Properties info) {
309312
StringBuilder res = new StringBuilder(url);
310313
for (Entry<Object, Object> entry : info.entrySet()) {
311314
if (entry.getValue() instanceof String && !"".equals(entry.getValue())) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.jdbc;
18+
19+
import static org.junit.Assert.assertEquals;
20+
21+
import com.google.cloud.spanner.connection.AbstractMockServerTest;
22+
import java.sql.Connection;
23+
import java.sql.SQLException;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.junit.runners.JUnit4;
27+
28+
@RunWith(JUnit4.class)
29+
public class JdbcDataSourceTest extends AbstractMockServerTest {
30+
31+
@Override
32+
protected String getBaseUrl() {
33+
return String.format(
34+
"jdbc:cloudspanner://localhost:%d/projects/p/instances/i/databases/d?usePlainText=true",
35+
getPort());
36+
}
37+
38+
@Test
39+
public void testGetConnectionFromNewDataSource() throws SQLException {
40+
for (boolean autoCommit : new boolean[] {true, false}) {
41+
JdbcDataSource dataSource = new JdbcDataSource();
42+
dataSource.setUrl(getBaseUrl());
43+
dataSource.setAutocommit(autoCommit);
44+
try (Connection connection = dataSource.getConnection()) {
45+
assertEquals(autoCommit, connection.getAutoCommit());
46+
}
47+
}
48+
}
49+
50+
@Test
51+
public void testGetConnectionFromCachedDataSource() throws SQLException {
52+
JdbcDataSource dataSource = new JdbcDataSource();
53+
dataSource.setUrl(getBaseUrl());
54+
for (boolean autoCommit : new boolean[] {true, false}) {
55+
// Changing a property on the DataSource should invalidate the internally cached
56+
// ConnectionOptions.
57+
dataSource.setAutocommit(autoCommit);
58+
try (Connection connection = dataSource.getConnection()) {
59+
assertEquals(autoCommit, connection.getAutoCommit());
60+
}
61+
}
62+
}
63+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.level=INFO
2+
.handlers=java.util.logging.ConsoleHandler
3+
java.util.logging.ConsoleHandler.level=INFO
4+
java.util.logging.Logger.useParentHandlers=true
5+
6+
# Set log level to WARN for SpannerImpl to prevent log spamming of the Spanner configuration.
7+
com.google.cloud.spanner.SpannerImpl.LEVEL=WARN

0 commit comments

Comments
 (0)