Skip to content

Commit ad15e89

Browse files
fix: session leak for invalid update (googleapis#1323)
* deps: update dependency com.google.cloud:google-cloud-spanner-bom to v6.45.2 (googleapis#1318) * fix: session leak for invalid update Close the underlying ResultSet if the result that was returned for an executeUpdate call turned out to be of an unexpected type. --------- Co-authored-by: Mend Renovate <bot@renovateapp.com>
1 parent 5186093 commit ad15e89

2 files changed

Lines changed: 128 additions & 2 deletions

File tree

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ public long executeLargeUpdate(String sql) throws SQLException {
9292
StatementResult result = execute(statement);
9393
switch (result.getResultType()) {
9494
case RESULT_SET:
95-
throw JdbcSqlExceptionFactory.of(
96-
"The statement is not a non-returning DML or DDL statement", Code.INVALID_ARGUMENT);
95+
// Close the result set as we are not going to return it to the user. This prevents the
96+
// underlying session from potentially being leaked.
97+
throw closeResultSetAndCreateInvalidQueryException(result);
9798
case UPDATE_COUNT:
9899
return result.getUpdateCount();
99100
case NO_RESULT:
@@ -104,6 +105,17 @@ public long executeLargeUpdate(String sql) throws SQLException {
104105
}
105106
}
106107

108+
private SQLException closeResultSetAndCreateInvalidQueryException(StatementResult result) {
109+
//noinspection finally
110+
try {
111+
result.getResultSet().close();
112+
} finally {
113+
//noinspection ReturnInsideFinallyBlock
114+
return JdbcSqlExceptionFactory.of(
115+
"The statement is not a non-returning DML or DDL statement", Code.INVALID_ARGUMENT);
116+
}
117+
}
118+
107119
/**
108120
* Adds a THEN RETURN/RETURNING clause to the given statement if the following conditions are all
109121
* met:
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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.assertThrows;
21+
import static org.junit.Assert.assertTrue;
22+
23+
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
24+
import com.google.cloud.spanner.SessionPoolOptions;
25+
import com.google.cloud.spanner.connection.AbstractMockServerTest;
26+
import com.google.cloud.spanner.connection.ConnectionOptions;
27+
import com.google.protobuf.ListValue;
28+
import com.google.protobuf.Value;
29+
import com.google.rpc.Code;
30+
import com.google.spanner.v1.ResultSetMetadata;
31+
import com.google.spanner.v1.StructType;
32+
import com.google.spanner.v1.StructType.Field;
33+
import com.google.spanner.v1.Type;
34+
import com.google.spanner.v1.TypeCode;
35+
import java.sql.Connection;
36+
import java.sql.SQLException;
37+
import org.junit.Before;
38+
import org.junit.Test;
39+
import org.junit.runner.RunWith;
40+
import org.junit.runners.JUnit4;
41+
42+
/**
43+
* Test class for verifying that the methods execute, executeQuery, and executeUpdate work as
44+
* intended.
45+
*/
46+
@RunWith(JUnit4.class)
47+
public class ExecuteMockServerTest extends AbstractMockServerTest {
48+
private static final String QUERY = "select * from my_table";
49+
50+
@Before
51+
public void setupResults() {
52+
super.setupResults();
53+
com.google.spanner.v1.ResultSet resultSet =
54+
com.google.spanner.v1.ResultSet.newBuilder()
55+
.setMetadata(
56+
ResultSetMetadata.newBuilder()
57+
.setRowType(
58+
StructType.newBuilder()
59+
.addFields(
60+
Field.newBuilder()
61+
.setType(Type.newBuilder().setCode(TypeCode.INT64).build())
62+
.setName("id")
63+
.build())
64+
.addFields(
65+
Field.newBuilder()
66+
.setType(Type.newBuilder().setCode(TypeCode.STRING).build())
67+
.setName("value")
68+
.build())
69+
.build())
70+
.build())
71+
.addRows(
72+
ListValue.newBuilder()
73+
.addValues(Value.newBuilder().setStringValue("1").build())
74+
.addValues(Value.newBuilder().setStringValue("One").build())
75+
.build())
76+
.build();
77+
mockSpanner.putStatementResult(
78+
StatementResult.query(com.google.cloud.spanner.Statement.of(QUERY), resultSet));
79+
}
80+
81+
private String createUrl() {
82+
return String.format(
83+
"jdbc:cloudspanner://localhost:%d/projects/%s/instances/%s/databases/%s?usePlainText=true",
84+
getPort(), "proj", "inst", "db");
85+
}
86+
87+
@Test
88+
public void testInvalidExecuteUpdate_shouldNotLeakSession() throws SQLException {
89+
int maxSessions = 1;
90+
try (Connection connection =
91+
new JdbcConnection(
92+
createUrl(),
93+
ConnectionOptions.newBuilder()
94+
.setUri(createUrl().substring("jdbc:".length()))
95+
.setSessionPoolOptions(
96+
SessionPoolOptions.newBuilder()
97+
.setMinSessions(1)
98+
.setMaxSessions(maxSessions)
99+
.setFailIfPoolExhausted()
100+
.build())
101+
.build())) {
102+
103+
for (int i = 0; i < (maxSessions + 1); i++) {
104+
SQLException exception =
105+
assertThrows(
106+
SQLException.class, () -> connection.createStatement().executeUpdate(QUERY));
107+
assertTrue(exception instanceof JdbcSqlException);
108+
JdbcSqlException jdbcSqlException = (JdbcSqlException) exception;
109+
// This would be RESOURCE_EXHAUSTED if the query leaked a session.
110+
assertEquals(Code.INVALID_ARGUMENT, jdbcSqlException.getCode());
111+
}
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)