Skip to content

Commit 988ccb3

Browse files
committed
fix: rollback could return session to pool twice
Transaction rollbacks could cause a session to be added to the session pool twice. This again could cause two transactions to try to use the same session at the same time, which again could lead to unpredictable errors.
1 parent 5fe67e0 commit 988ccb3

File tree

9 files changed

+386
-19
lines changed

9 files changed

+386
-19
lines changed

cloud-spanner-r2dbc/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@
8383
<scope>provided</scope>
8484
</dependency>
8585

86+
<!-- Test dependencies for a mock Spanner server -->
87+
<dependency>
88+
<groupId>com.google.cloud</groupId>
89+
<artifactId>google-cloud-spanner</artifactId>
90+
<type>test-jar</type>
91+
<scope>test</scope>
92+
</dependency>
93+
<dependency>
94+
<groupId>com.google.api</groupId>
95+
<artifactId>gax-grpc</artifactId>
96+
<classifier>testlib</classifier>
97+
<scope>test</scope>
98+
</dependency>
99+
86100
</dependencies>
87101

88102
<build>

cloud-spanner-r2dbc/src/main/java/com/google/cloud/spanner/r2dbc/SpannerConnectionConfiguration.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.auth.oauth2.OAuth2Credentials;
2121
import com.google.cloud.spanner.SpannerOptions;
2222
import com.google.cloud.spanner.r2dbc.util.Assert;
23+
import io.grpc.ManagedChannelBuilder;
2324
import io.r2dbc.spi.ConnectionFactoryOptions;
2425
import io.r2dbc.spi.R2dbcNonTransientResourceException;
2526
import java.io.IOException;
@@ -51,11 +52,13 @@ public class SpannerConnectionConfiguration {
5152
// TODO: check how to handle full URL (it gets parsed by SPI, we only get pieces)
5253
private final String fullyQualifiedDbName;
5354

54-
private String projectId;
55+
private final ConnectionFactoryOptions options;
5556

56-
private String instanceName;
57+
private final String projectId;
5758

58-
private String databaseName;
59+
private final String instanceName;
60+
61+
private final String databaseName;
5962

6063
private final OAuth2Credentials credentials;
6164

@@ -82,6 +85,7 @@ public class SpannerConnectionConfiguration {
8285
* @param credentials GCP credentials to authenticate service calls with.
8386
*/
8487
private SpannerConnectionConfiguration(
88+
ConnectionFactoryOptions options,
8589
String projectId,
8690
String instanceName,
8791
String databaseName,
@@ -91,6 +95,7 @@ private SpannerConnectionConfiguration(
9195
Assert.requireNonNull(instanceName, "instanceName must not be null");
9296
Assert.requireNonNull(databaseName, "databaseName must not be null");
9397

98+
this.options = Assert.requireNonNull(options, "options must not be null");
9499
this.projectId = projectId;
95100
this.instanceName = instanceName;
96101
this.databaseName = databaseName;
@@ -189,13 +194,24 @@ public int hashCode() {
189194
public SpannerOptions buildSpannerOptions() {
190195
SpannerOptions.Builder optionsBuilder = SpannerOptions.newBuilder();
191196

197+
if (this.options.hasOption(ConnectionFactoryOptions.HOST)) {
198+
int port = 443;
199+
if (this.options.hasOption(ConnectionFactoryOptions.PORT)) {
200+
port = (Integer) Objects.requireNonNull(
201+
this.options.getValue(ConnectionFactoryOptions.PORT));
202+
}
203+
String host = String.format("http://%s:%d", this.options.getValue(ConnectionFactoryOptions.HOST), port);
204+
optionsBuilder.setHost(host);
205+
}
192206
if (this.projectId != null) {
193207
optionsBuilder.setProjectId(this.projectId);
194208
}
195-
196209
if (this.credentials != null) {
197210
optionsBuilder.setCredentials(this.credentials);
198211
}
212+
if (this.usePlainText) {
213+
optionsBuilder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
214+
}
199215

200216
optionsBuilder.setHeaderProvider(() ->
201217
Collections.singletonMap(USER_AGENT_KEY, USER_AGENT_LIBRARY_NAME + "/" + PACKAGE_VERSION));
@@ -210,6 +226,7 @@ public SpannerOptions buildSpannerOptions() {
210226
* Builder for the enclosing class.
211227
*/
212228
public static class Builder {
229+
private final ConnectionFactoryOptions options;
213230

214231
private String fullyQualifiedDatabaseName;
215232

@@ -235,6 +252,10 @@ public static class Builder {
235252

236253
private boolean autocommit = true;
237254

255+
public Builder(ConnectionFactoryOptions options) {
256+
this.options = Assert.requireNonNull(options, "options must not be null");
257+
}
258+
238259
/**
239260
* R2DBC SPI does not provide the full URL to drivers after parsing the connection string.
240261
* Therefore, this usecase is only possible if the client application provides a URL property
@@ -359,7 +380,7 @@ public SpannerConnectionConfiguration build() {
359380
}
360381

361382
SpannerConnectionConfiguration configuration = new SpannerConnectionConfiguration(
362-
this.projectId, this.instanceName, this.databaseName, this.credentials);
383+
this.options, this.projectId, this.instanceName, this.databaseName, this.credentials);
363384

364385
configuration.partialResultSetFetchSize = this.partialResultSetFetchSize;
365386
configuration.ddlOperationTimeout = this.ddlOperationTimeout;

cloud-spanner-r2dbc/src/main/java/com/google/cloud/spanner/r2dbc/SpannerConnectionFactoryProvider.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
2323
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
2424

25-
import com.google.auth.oauth2.GoogleCredentials;
2625
import com.google.auth.oauth2.OAuth2Credentials;
2726
import com.google.cloud.NoCredentials;
2827
import com.google.cloud.spanner.r2dbc.util.Assert;
@@ -68,7 +67,7 @@ public class SpannerConnectionFactoryProvider implements ConnectionFactoryProvid
6867
/**
6968
* Option specifying the already-instantiated credentials object.
7069
*/
71-
public static final Option<GoogleCredentials> GOOGLE_CREDENTIALS =
70+
public static final Option<OAuth2Credentials> GOOGLE_CREDENTIALS =
7271
Option.valueOf("google_credentials");
7372

7473
public static final Option<Boolean> AUTOCOMMIT = Option.valueOf(AUTOCOMMIT_PROPERTY_NAME);
@@ -117,7 +116,8 @@ public String getDriver() {
117116
SpannerConnectionConfiguration createConfiguration(
118117
ConnectionFactoryOptions options) {
119118

120-
SpannerConnectionConfiguration.Builder config = new SpannerConnectionConfiguration.Builder();
119+
SpannerConnectionConfiguration.Builder config =
120+
new SpannerConnectionConfiguration.Builder(options);
121121

122122
// Directly passed URL is supported for backwards compatibility. R2DBC SPI does not provide
123123
// the original URL when creating connection through ConnectionFactories.get(String).

cloud-spanner-r2dbc/src/main/java/com/google/cloud/spanner/r2dbc/v2/DatabaseClientTransactionManager.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.cloud.spanner.ReadOnlyTransaction;
3030
import com.google.cloud.spanner.TimestampBound;
3131
import com.google.cloud.spanner.TransactionContext;
32+
import com.google.cloud.spanner.TransactionManager.TransactionState;
3233
import com.google.cloud.spanner.r2dbc.TransactionInProgressException;
3334
import java.util.function.Function;
3435
import org.slf4j.Logger;
@@ -108,7 +109,9 @@ ApiFuture<Void> clearTransactionManager() {
108109
ApiFuture<Void> returnFuture = ApiFutures.immediateFuture(null);
109110

110111
if (this.transactionManager != null) {
111-
returnFuture = this.transactionManager.closeAsync();
112+
if (this.transactionManager.getState() != TransactionState.ROLLED_BACK) {
113+
returnFuture = this.transactionManager.closeAsync();
114+
}
112115
this.transactionManager = null;
113116
}
114117

cloud-spanner-r2dbc/src/test/java/com/google/cloud/spanner/r2dbc/SpannerConnectionConfigurationTest.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
import static org.mockito.Mockito.mock;
2122

2223
import com.google.auth.oauth2.GoogleCredentials;
2324
import com.google.cloud.NoCredentials;
2425
import com.google.cloud.spanner.r2dbc.SpannerConnectionConfiguration.Builder;
26+
import io.r2dbc.spi.ConnectionFactoryOptions;
2527
import java.time.Duration;
2628
import org.junit.jupiter.api.BeforeEach;
2729
import org.junit.jupiter.api.Test;
28-
import org.mockito.Mockito;
2930

3031
/**
3132
* Test for {@link SpannerConnectionConfiguration}.
3233
*/
3334
class SpannerConnectionConfigurationTest {
3435

35-
GoogleCredentials mockCredentials = Mockito.mock(GoogleCredentials.class);
36+
GoogleCredentials mockCredentials = mock(GoogleCredentials.class);
3637

3738
SpannerConnectionConfiguration.Builder configurationBuilder;
3839

@@ -41,13 +42,15 @@ class SpannerConnectionConfigurationTest {
4142
*/
4243
@BeforeEach
4344
public void setUpMockCredentials() {
44-
this.configurationBuilder = new SpannerConnectionConfiguration.Builder()
45+
this.configurationBuilder = new SpannerConnectionConfiguration.Builder(
46+
mock(ConnectionFactoryOptions.class))
4547
.setCredentials(this.mockCredentials);
4648
}
4749

4850
@Test
4951
void missingInstanceNameTriggersException() {
50-
Builder builder = new SpannerConnectionConfiguration.Builder()
52+
Builder builder = new SpannerConnectionConfiguration.Builder(
53+
mock(ConnectionFactoryOptions.class))
5154
.setProjectId("project1")
5255
.setDatabaseName("db")
5356
.setCredentials(NoCredentials.getInstance());
@@ -59,7 +62,8 @@ void missingInstanceNameTriggersException() {
5962

6063
@Test
6164
void missingDatabaseNameTriggersException() {
62-
Builder builder = new SpannerConnectionConfiguration.Builder()
65+
Builder builder = new SpannerConnectionConfiguration.Builder(
66+
mock(ConnectionFactoryOptions.class))
6367
.setProjectId("project1")
6468
.setInstanceName("an-instance")
6569
.setCredentials(NoCredentials.getInstance());
@@ -71,7 +75,8 @@ void missingDatabaseNameTriggersException() {
7175

7276
@Test
7377
void missingProjectIdTriggersException() {
74-
Builder builder = new SpannerConnectionConfiguration.Builder()
78+
Builder builder = new SpannerConnectionConfiguration.Builder(
79+
mock(ConnectionFactoryOptions.class))
7580
.setInstanceName("an-instance")
7681
.setDatabaseName("db")
7782
.setCredentials(NoCredentials.getInstance());

cloud-spanner-r2dbc/src/test/java/com/google/cloud/spanner/r2dbc/SpannerConnectionFactoryProviderTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@
4848
import com.google.spanner.v1.StructType.Field;
4949
import com.google.spanner.v1.Type;
5050
import com.google.spanner.v1.TypeCode;
51+
import io.r2dbc.spi.Closeable;
5152
import io.r2dbc.spi.ConnectionFactories;
5253
import io.r2dbc.spi.ConnectionFactory;
5354
import io.r2dbc.spi.ConnectionFactoryOptions;
5455
import org.junit.jupiter.api.BeforeEach;
5556
import org.junit.jupiter.api.Test;
57+
import reactor.core.publisher.Flux;
5658

5759
/**
5860
* Unit test for {@link SpannerConnectionFactoryProvider}.
@@ -101,6 +103,8 @@ void testCreate() {
101103
this.spannerConnectionFactoryProvider.create(SPANNER_OPTIONS);
102104
assertThat(spannerConnectionFactory).isNotNull();
103105
assertThat(spannerConnectionFactory).isInstanceOf(SpannerClientLibraryConnectionFactory.class);
106+
107+
Flux.from(((Closeable) spannerConnectionFactory).close()).blockLast();
104108
}
105109

106110
@Test
@@ -111,6 +115,8 @@ void testCreateFactoryWithOldDriverNameWillReturnCorrectFactory() {
111115
assertThat(spannerConnectionFactory)
112116
.isNotNull()
113117
.isInstanceOf(SpannerClientLibraryConnectionFactory.class);
118+
119+
Flux.from(((Closeable) spannerConnectionFactory).close()).blockLast();
114120
}
115121

116122
@Test
@@ -121,6 +127,8 @@ void testCreateFactoryWithDriverNameWillReturnCorrectFactory() {
121127
assertThat(spannerConnectionFactory)
122128
.isNotNull()
123129
.isInstanceOf(SpannerClientLibraryConnectionFactory.class);
130+
131+
Flux.from(((Closeable) spannerConnectionFactory).close()).blockLast();
124132
}
125133

126134
@Test
@@ -138,6 +146,8 @@ void testCreateFactoryWithUrl() {
138146
assertThat(spannerConnectionFactory)
139147
.isNotNull()
140148
.isInstanceOf(SpannerClientLibraryConnectionFactory.class);
149+
150+
Flux.from(((Closeable) spannerConnectionFactory).close()).blockLast();
141151
}
142152

143153
@Test

cloud-spanner-r2dbc/src/test/java/com/google/cloud/spanner/r2dbc/v2/DatabaseClientReactiveAdapterTest.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.google.common.collect.ImmutableList;
4646
import com.google.common.util.concurrent.Futures;
4747
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
48+
import io.r2dbc.spi.ConnectionFactoryOptions;
4849
import java.util.concurrent.ExecutorService;
4950
import java.util.concurrent.Executors;
5051
import org.junit.jupiter.api.AfterEach;
@@ -74,7 +75,7 @@ class DatabaseClientReactiveAdapterTest {
7475
@BeforeEach
7576
void setup() throws Exception {
7677
this.config =
77-
new SpannerConnectionConfiguration.Builder()
78+
new SpannerConnectionConfiguration.Builder(mock(ConnectionFactoryOptions.class))
7879
.setFullyQualifiedDatabaseName("projects/p/instances/i/databases/d")
7980
.setCredentials(mock(GoogleCredentials.class))
8081
.build();
@@ -148,7 +149,8 @@ void testSameAutocommitNoop() {
148149

149150
@Test
150151
void unsetQueryOptimizerResultsInDefaultQueryOptions() {
151-
SpannerConnectionConfiguration config = new SpannerConnectionConfiguration.Builder()
152+
SpannerConnectionConfiguration config = new SpannerConnectionConfiguration.Builder(
153+
mock(ConnectionFactoryOptions.class))
152154
.setFullyQualifiedDatabaseName("projects/p/instances/i/databases/d")
153155
.setCredentials(mock(GoogleCredentials.class))
154156
.build();
@@ -160,7 +162,8 @@ void unsetQueryOptimizerResultsInDefaultQueryOptions() {
160162

161163
@Test
162164
void queryOptimizerPropagatesToQueryOptions() {
163-
SpannerConnectionConfiguration config = new SpannerConnectionConfiguration.Builder()
165+
SpannerConnectionConfiguration config = new SpannerConnectionConfiguration.Builder(
166+
mock(ConnectionFactoryOptions.class))
164167
.setFullyQualifiedDatabaseName("projects/p/instances/i/databases/d")
165168
.setCredentials(mock(GoogleCredentials.class))
166169
.setOptimizerVersion("2")

0 commit comments

Comments
 (0)