Skip to content

Commit 107ec66

Browse files
authored
perf: use multiplexed sessions (#1673)
* perf: use multiplexed sessions Enable the use of multiplexed sessions by default for queries in auto-commit mode. Multiplexed sessions can handle any number of queries concurrently. This means that the JDBC driver does not need to check out a session exclusively from the internal session pool in order to execute a query. Instead, a single multiplexed session is enough for all queries that are executed by all JDBC connections that connect to the same Spanner database. This allows a higher degree of parallelism to be achieved from a single client machine. Note that due to how the JDBC API is defined, each JDBC connection can only execute one query at a time. If you for example want to execute 1000 queries in parallel, then you also need to create 1000 JDBC connections. Spanner JDBC connection are however lightweight, as each JDBC connection internally uses a pool of gRPC channels. It is recommended to enable the use of virtual threads to achieve the highest possible degree of parallelism with the JDBC driver. This option can be set by adding useVirtualThreads=true to the JDBC connection URL. Note that virtual threads are only supported on Java 21 and higher. * test: add test for multi-use read-only transaction
1 parent 2cdc0a3 commit 107ec66

File tree

5 files changed

+285
-1
lines changed

5 files changed

+285
-1
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2024 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;
18+
19+
import com.google.api.core.InternalApi;
20+
21+
/**
22+
* This class is only here to access a package-private method in the Spanner client library and will
23+
* be removed in the future.
24+
*/
25+
@InternalApi
26+
public class SessionPoolOptionsHelper {
27+
private SessionPoolOptionsHelper() {}
28+
29+
@InternalApi
30+
public static SessionPoolOptions.Builder useMultiplexedSessions(
31+
SessionPoolOptions.Builder builder) {
32+
return builder.setUseMultiplexedSession(true);
33+
}
34+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import com.google.api.core.InternalApi;
2020
import com.google.auth.oauth2.GoogleCredentials;
21+
import com.google.cloud.spanner.SessionPoolOptions;
22+
import com.google.cloud.spanner.SessionPoolOptionsHelper;
2123
import com.google.cloud.spanner.SpannerException;
2224
import com.google.cloud.spanner.connection.ConnectionOptions;
2325
import com.google.cloud.spanner.connection.ConnectionOptions.ConnectionProperty;
@@ -239,6 +241,9 @@ private ConnectionOptions buildConnectionOptions(String connectionUrl, Propertie
239241
&& info.get(OPEN_TELEMETRY_PROPERTY_KEY) instanceof OpenTelemetry) {
240242
builder.setOpenTelemetry((OpenTelemetry) info.get(OPEN_TELEMETRY_PROPERTY_KEY));
241243
}
244+
// Enable multiplexed sessions by default for the JDBC driver.
245+
builder.setSessionPoolOptions(
246+
SessionPoolOptionsHelper.useMultiplexedSessions(SessionPoolOptions.newBuilder()).build());
242247
return builder.build();
243248
}
244249

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2024 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;
18+
19+
import com.google.spanner.v1.Session;
20+
21+
public class MockServerHelper {
22+
23+
private MockServerHelper() {}
24+
25+
public static Session getSession(MockSpannerServiceImpl server, String sessionName) {
26+
return server.getSession(sessionName);
27+
}
28+
}

src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionUrlTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public static void reset() {
6868
}
6969

7070
protected String getBaseUrl() {
71-
return super.getBaseUrl() + ";maxSessions=1";
71+
return super.getBaseUrl() + ";minSessions=0;maxSessions=1";
7272
}
7373

7474
@Test
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright 2023 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+
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import com.google.cloud.spanner.Dialect;
25+
import com.google.cloud.spanner.MockServerHelper;
26+
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
27+
import com.google.cloud.spanner.connection.AbstractMockServerTest;
28+
import com.google.cloud.spanner.connection.SpannerPool;
29+
import com.google.spanner.v1.CreateSessionRequest;
30+
import com.google.spanner.v1.ExecuteSqlRequest;
31+
import com.google.spanner.v1.Session;
32+
import java.sql.Connection;
33+
import java.sql.DriverManager;
34+
import java.sql.ResultSet;
35+
import java.sql.SQLException;
36+
import org.junit.After;
37+
import org.junit.Before;
38+
import org.junit.Test;
39+
import org.junit.runner.RunWith;
40+
import org.junit.runners.Parameterized;
41+
import org.junit.runners.Parameterized.Parameter;
42+
import org.junit.runners.Parameterized.Parameters;
43+
44+
@RunWith(Parameterized.class)
45+
public class MultiplexedSessionsMockServerTest extends AbstractMockServerTest {
46+
private static final String SELECT_RANDOM_SQL = SELECT_RANDOM_STATEMENT.getSql();
47+
48+
private static final String INSERT_SQL = INSERT_STATEMENT.getSql();
49+
50+
@Parameter public Dialect dialect;
51+
52+
private Dialect currentDialect;
53+
54+
@Parameters(name = "dialect = {0}")
55+
public static Object[] data() {
56+
return Dialect.values();
57+
}
58+
59+
@Before
60+
public void setupDialect() {
61+
if (this.dialect != currentDialect) {
62+
mockSpanner.putStatementResult(StatementResult.detectDialectResult(this.dialect));
63+
this.currentDialect = dialect;
64+
}
65+
}
66+
67+
@After
68+
public void clearRequests() {
69+
mockSpanner.clearRequests();
70+
SpannerPool.closeSpannerPool();
71+
}
72+
73+
private String createUrl() {
74+
return String.format(
75+
"jdbc:cloudspanner://localhost:%d/projects/%s/instances/%s/databases/%s?usePlainText=true",
76+
getPort(), "proj", "inst", "db" + (dialect == Dialect.POSTGRESQL ? "pg" : ""));
77+
}
78+
79+
private Connection createConnection() throws SQLException {
80+
return DriverManager.getConnection(createUrl());
81+
}
82+
83+
@Test
84+
public void testUsesMultiplexedSessionForQueryInAutoCommit() throws SQLException {
85+
try (Connection connection = createConnection()) {
86+
assertTrue(connection.getAutoCommit());
87+
try (ResultSet resultSet = connection.createStatement().executeQuery(SELECT_RANDOM_SQL)) {
88+
//noinspection StatementWithEmptyBody
89+
while (resultSet.next()) {
90+
// Just consume the results
91+
}
92+
}
93+
}
94+
// Verify that one multiplexed session was created and used.
95+
assertEquals(1, mockSpanner.countRequestsOfType(CreateSessionRequest.class));
96+
CreateSessionRequest request = mockSpanner.getRequestsOfType(CreateSessionRequest.class).get(0);
97+
assertTrue(request.getSession().getMultiplexed());
98+
assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
99+
String sessionId = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0).getSession();
100+
Session session = MockServerHelper.getSession(mockSpanner, sessionId);
101+
assertNotNull(session);
102+
assertTrue(session.getMultiplexed());
103+
}
104+
105+
@Test
106+
public void testUsesMultiplexedSessionForQueryInReadOnlyTransaction() throws SQLException {
107+
int numQueries = 2;
108+
try (Connection connection = createConnection()) {
109+
connection.setReadOnly(true);
110+
connection.setAutoCommit(false);
111+
112+
for (int ignore = 0; ignore < numQueries; ignore++) {
113+
try (ResultSet resultSet = connection.createStatement().executeQuery(SELECT_RANDOM_SQL)) {
114+
//noinspection StatementWithEmptyBody
115+
while (resultSet.next()) {
116+
// Just consume the results
117+
}
118+
}
119+
}
120+
}
121+
// Verify that one multiplexed session was created and used.
122+
assertEquals(1, mockSpanner.countRequestsOfType(CreateSessionRequest.class));
123+
CreateSessionRequest request = mockSpanner.getRequestsOfType(CreateSessionRequest.class).get(0);
124+
assertTrue(request.getSession().getMultiplexed());
125+
126+
// Verify that both queries used the multiplexed session.
127+
assertEquals(numQueries, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
128+
for (int index = 0; index < numQueries; index++) {
129+
String sessionId =
130+
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(index).getSession();
131+
Session session = MockServerHelper.getSession(mockSpanner, sessionId);
132+
assertNotNull(session);
133+
assertTrue(session.getMultiplexed());
134+
}
135+
}
136+
137+
@Test
138+
public void testUsesRegularSessionForDmlInAutoCommit() throws SQLException {
139+
try (Connection connection = createConnection()) {
140+
assertTrue(connection.getAutoCommit());
141+
assertEquals(1, connection.createStatement().executeUpdate(INSERT_SQL));
142+
}
143+
// The JDBC connection creates a multiplexed session by default, because it executes a query to
144+
// check what dialect the database uses. This query is executed using a multiplexed session.
145+
assertEquals(1, mockSpanner.countRequestsOfType(CreateSessionRequest.class));
146+
CreateSessionRequest request = mockSpanner.getRequestsOfType(CreateSessionRequest.class).get(0);
147+
assertTrue(request.getSession().getMultiplexed());
148+
// Verify that a regular session was used for the insert statement.
149+
assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
150+
assertEquals(
151+
INSERT_SQL, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0).getSql());
152+
String sessionId = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0).getSession();
153+
Session session = MockServerHelper.getSession(mockSpanner, sessionId);
154+
assertNotNull(session);
155+
assertFalse(session.getMultiplexed());
156+
}
157+
158+
@Test
159+
public void testUsesRegularSessionForQueryInTransaction() throws SQLException {
160+
try (Connection connection = createConnection()) {
161+
connection.setAutoCommit(false);
162+
assertFalse(connection.getAutoCommit());
163+
164+
try (ResultSet resultSet = connection.createStatement().executeQuery(SELECT_RANDOM_SQL)) {
165+
//noinspection StatementWithEmptyBody
166+
while (resultSet.next()) {
167+
// Just consume the results
168+
}
169+
}
170+
connection.commit();
171+
}
172+
// The JDBC connection creates a multiplexed session by default, because it executes a query to
173+
// check what dialect the database uses. This query is executed using a multiplexed session.
174+
assertEquals(1, mockSpanner.countRequestsOfType(CreateSessionRequest.class));
175+
CreateSessionRequest request = mockSpanner.getRequestsOfType(CreateSessionRequest.class).get(0);
176+
assertTrue(request.getSession().getMultiplexed());
177+
// Verify that a regular session was used for the select statement.
178+
assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
179+
assertEquals(
180+
SELECT_RANDOM_SQL, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0).getSql());
181+
String sessionId = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0).getSession();
182+
Session session = MockServerHelper.getSession(mockSpanner, sessionId);
183+
assertNotNull(session);
184+
assertFalse(session.getMultiplexed());
185+
}
186+
187+
@Test
188+
public void testUsesMultiplexedSessionInCombinationWithSessionPoolOptions() throws SQLException {
189+
// Create a connection that uses a session pool with MinSessions=0.
190+
// This should stop any regular sessions from being created.
191+
// TODO: Modify this test once https://github.com/googleapis/java-spanner/pull/3197 has been
192+
// released.
193+
try (Connection connection = DriverManager.getConnection(createUrl() + ";minSessions=0")) {
194+
assertTrue(connection.getAutoCommit());
195+
try (ResultSet resultSet = connection.createStatement().executeQuery(SELECT_RANDOM_SQL)) {
196+
//noinspection StatementWithEmptyBody
197+
while (resultSet.next()) {
198+
// Just consume the results
199+
}
200+
}
201+
}
202+
// TODO: Remove this line once https://github.com/googleapis/java-spanner/pull/3197 has been
203+
// released.
204+
// Adding 'minSessions=X' or 'maxSessions=x' to the connection URL currently disables the use of
205+
// multiplexed sessions due to a bug in the Spanner Java client.
206+
assertEquals(0, mockSpanner.countRequestsOfType(CreateSessionRequest.class));
207+
208+
// Verify that one multiplexed session was created and used.
209+
// TODO: Uncomment
210+
// assertEquals(1, mockSpanner.countRequestsOfType(CreateSessionRequest.class));
211+
// CreateSessionRequest request =
212+
// mockSpanner.getRequestsOfType(CreateSessionRequest.class).get(0);
213+
// assertTrue(request.getSession().getMultiplexed());
214+
// // There should be no regular sessions in use.
215+
// assertEquals(0, mockSpanner.countRequestsOfType(BatchCreateSessionsRequest.class));
216+
}
217+
}

0 commit comments

Comments
 (0)