Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change autocommit/readonly value on connection creation #378

Merged
merged 2 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ The following options are available to be configured for the connection factory:
| `database` | Your Spanner Database name | True (if `url` not provided) | |
| `url` | A Cloud Spanner R2DBC URL specifying your Spanner database. An alternative to specifying `project`, `instance`, and `database` separately. | False |
| `google_credentials` | Optional [Google credentials](https://cloud.google.com/docs/authentication/production) override to specify for your Google Cloud account. | False | If not provided, credentials will be [inferred from your runtime environment](https://cloud.google.com/docs/authentication/production#finding_credentials_automatically).
| `autocommit` | Whether new connections are created in autocommit mode | False | True |
| `readonly` | Whether new connections start with a read-only transaction | False | False |
| `partial_result_set_fetch_size` | Number of intermediate result sets that are buffered in transit for a read query. | False | 1 |
| `ddl_operation_timeout` | Duration in seconds to wait for a DDL operation to complete before timing out | False | 600 seconds |
| `ddl_operation_poll_interval` | Duration in seconds to wait between each polling request for the completion of a DDL operation | False | 5 seconds |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public class SpannerConnectionConfiguration {

private String optimizerVersion;

private boolean readonly;

private boolean autocommit;

/**
* Basic property initializing constructor.
*
Expand Down Expand Up @@ -147,6 +151,14 @@ public String getOptimizerVersion() {
return this.optimizerVersion;
}

public boolean isReadonly() {
return this.readonly;
}

public boolean isAutocommit() {
return this.autocommit;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down Expand Up @@ -227,6 +239,10 @@ public static class Builder {

private String optimizerVersion;

private boolean readonly = false;

private boolean autocommit = true;

/**
* R2DBC SPI does not provide the full URL to drivers after parsing the connection string.
* Therefore, this usecase is only possible if the client application provides a URL property
Expand Down Expand Up @@ -317,6 +333,16 @@ public Builder setOptimizerVersion(String optimizerVersion) {
return this;
}

public Builder setReadonly(boolean readonly) {
this.readonly = readonly;
return this;
}

public Builder setAutocommit(boolean autocommit) {
this.autocommit = autocommit;
return this;
}

/**
* Constructs an instance of the {@link SpannerConnectionConfiguration}.
*
Expand Down Expand Up @@ -357,6 +383,8 @@ public SpannerConnectionConfiguration build() {
: Runtime.getRuntime().availableProcessors();
configuration.usePlainText = this.usePlainText;
configuration.optimizerVersion = this.optimizerVersion;
configuration.readonly = this.readonly;
configuration.autocommit = this.autocommit;

return configuration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.google.cloud.spanner.r2dbc;

import static com.google.cloud.spanner.connection.ConnectionOptions.AUTOCOMMIT_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.READONLY_PROPERTY_NAME;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionConfiguration.FQDN_PATTERN_PARSE;
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
Expand Down Expand Up @@ -80,6 +82,11 @@ public class SpannerConnectionFactoryProvider implements ConnectionFactoryProvid
public static final Option<GoogleCredentials> GOOGLE_CREDENTIALS =
Option.valueOf("google_credentials");

public static final Option<Boolean> AUTOCOMMIT = Option.valueOf(AUTOCOMMIT_PROPERTY_NAME);

public static final Option<Boolean> READONLY = Option.valueOf(READONLY_PROPERTY_NAME);


/**
* Option specifying the location of the GCP credentials file. Same as GOOGLE_CREDENTIALS,
* but consistent with the JDBC driver option.
Expand Down Expand Up @@ -186,9 +193,27 @@ SpannerConnectionConfiguration createConfiguration(
config.setOptimizerVersion(options.getValue(OPTIMIZER_VERSION));
}

if (options.hasOption(AUTOCOMMIT)) {
config.setAutocommit(getBooleanFlag(options.getValue(AUTOCOMMIT)));
}

if (options.hasOption(READONLY)) {
config.setReadonly(getBooleanFlag(options.getValue(READONLY)));
}

return config.build();
}

private boolean getBooleanFlag(Object value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method necessary? I think options.getValue(READONLY) will always return a boolean if it's declared as a Option<Boolean> initially. (maybe)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, no -- when the option is coming through a URL, it will get parsed as String. But R2DBC will likely address this in the future (r2dbc/r2dbc-spi#206), so I am hoping that this method is temporary.

Assert.requireNonNull(value, "Non-null option value expected");
if (value instanceof Boolean) {
return ((Boolean) value).booleanValue();
} else if (value instanceof String) {
return Boolean.valueOf((String) value);
}
throw new IllegalStateException("Flag type expected to be Boolean or String for " + value);
}

/**
* Extracts credentials from properties passed in either through URL or programmatically.
* Fails if more than one known security option is specified.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ public interface SpannerConnection {
* @return {@link Mono} signaling readonly transaction is ready for use
*/
Mono<Void> beginReadonlyTransaction();

boolean isInReadonlyTransaction();
}
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ QueryOptions getQueryOptions() {
return this.queryOptions;
}

boolean isInReadonlyTransaction() {
return this.txnManager.isInReadonlyTransaction();
}

@VisibleForTesting
void setTxnManager(DatabaseClientTransactionManager txnManager) {
this.txnManager = txnManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.google.cloud.spanner.r2dbc.v2;

import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.r2dbc.SpannerConnectionConfiguration;
import com.google.cloud.spanner.r2dbc.api.SpannerConnection;
import com.google.cloud.spanner.r2dbc.statement.StatementParser;
import com.google.cloud.spanner.r2dbc.statement.StatementType;
Expand All @@ -38,10 +37,8 @@ class SpannerClientLibraryConnection implements Connection, SpannerConnection {
* Cloud Spanner implementation of R2DBC Connection SPI.
*
* @param clientLibraryAdapter adapter to Cloud Spanner database client
* @param config driver configuration extracted from URL or passed directly to connection factory.
*/
public SpannerClientLibraryConnection(DatabaseClientReactiveAdapter clientLibraryAdapter,
SpannerConnectionConfiguration config) {
public SpannerClientLibraryConnection(DatabaseClientReactiveAdapter clientLibraryAdapter) {
this.clientLibraryAdapter = clientLibraryAdapter;
}

Expand Down Expand Up @@ -142,4 +139,8 @@ public Publisher<Boolean> validate(ValidationDepth depth) {
public Publisher<Void> close() {
return this.clientLibraryAdapter.close();
}

public boolean isInReadonlyTransaction() {
return this.clientLibraryAdapter.isInReadonlyTransaction();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,21 @@ public SpannerClientLibraryConnectionFactory(SpannerConnectionConfiguration conf

@Override
public Publisher<? extends Connection> create() {

return Mono.just(
Mono<SpannerClientLibraryConnection> connection = Mono.just(
new SpannerClientLibraryConnection(
new DatabaseClientReactiveAdapter(this.spannerClient, this.config),
this.config));
new DatabaseClientReactiveAdapter(this.spannerClient, this.config))
);

if (this.config.isReadonly()) {
connection = connection.delayUntil(conn -> conn.beginReadonlyTransaction());
}

// Autocommit is on by default; turn off if needed.
if (!this.config.isAutocommit()) {
connection = connection.delayUntil(conn -> conn.setAutoCommit(false));
}

return connection;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

package com.google.cloud.spanner.r2dbc;

import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.AUTOCOMMIT;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.CREDENTIALS;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.DRIVER_NAME;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.GOOGLE_CREDENTIALS;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.INSTANCE;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.OPTIMIZER_VERSION;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.PARTIAL_RESULT_SET_FETCH_SIZE;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.PROJECT;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.READONLY;
import static com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryProvider.URL;
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
Expand Down Expand Up @@ -72,6 +74,8 @@ class SpannerConnectionFactoryProviderTest {
.option(GOOGLE_CREDENTIALS, mock(GoogleCredentials.class))
.build();

ConnectionFactoryOptions.Builder optionsBuilder;

SpannerConnectionFactoryProvider spannerConnectionFactoryProvider;

Client mockClient;
Expand All @@ -94,6 +98,10 @@ public void setUp() {

this.mockCredentials = mock(GoogleCredentials.class);

this.optionsBuilder = ConnectionFactoryOptions.builder()
.option(DRIVER, "spanner")
.option(DATABASE, "projects/p/instances/i/databases/d")
.option(GOOGLE_CREDENTIALS, this.mockCredentials);
}

@Test
Expand Down Expand Up @@ -201,13 +209,7 @@ void testCreateFactoryWithClientLibraryClient() {
SpannerConnectionFactoryProvider customSpannerConnectionFactoryProvider
= new SpannerConnectionFactoryProvider();

ConnectionFactoryOptions options =
ConnectionFactoryOptions.builder()
.option(DRIVER, DRIVER_NAME)
.option(PROJECT, "project-id")
.option(INSTANCE, "an-instance")
.option(DATABASE, "db")
.option(GOOGLE_CREDENTIALS, mock(GoogleCredentials.class))
ConnectionFactoryOptions options = this.optionsBuilder
.option(Option.valueOf("client-implementation"), "client-library")
.build();

Expand Down Expand Up @@ -304,10 +306,7 @@ void multipleAuthenticationMethodsDisallowedProgrammatic() {

@Test
void passOptimizerVersion() {
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
.option(DATABASE, "projects/p/instances/i/databases/d")
.option(DRIVER, "spanner")
.option(GOOGLE_CREDENTIALS, this.mockCredentials)
ConnectionFactoryOptions options = this.optionsBuilder
.option(OPTIMIZER_VERSION, "2")
.build();

Expand All @@ -317,6 +316,47 @@ void passOptimizerVersion() {
assertEquals("2", config.getOptimizerVersion());
}

@Test
void readonlyFalseByDefault() {
SpannerConnectionConfiguration config =
this.spannerConnectionFactoryProvider.createConfiguration(this.optionsBuilder.build());

assertEquals(false, config.isReadonly());
}

@Test
void passReadonlyTrueOption() {
ConnectionFactoryOptions options = this.optionsBuilder
.option(READONLY, true)
.build();

SpannerConnectionConfiguration config =
this.spannerConnectionFactoryProvider.createConfiguration(options);

assertEquals(true, config.isReadonly());
}

@Test
void autocommitTrueByDefault() {
SpannerConnectionConfiguration config =
this.spannerConnectionFactoryProvider.createConfiguration(this.optionsBuilder.build());

assertEquals(true, config.isAutocommit());
}

@Test
void passAutocommitFalseOption() {
ConnectionFactoryOptions options = this.optionsBuilder
.option(AUTOCOMMIT, false)
.build();

SpannerConnectionConfiguration config =
this.spannerConnectionFactoryProvider.createConfiguration(options);

assertEquals(false, config.isAutocommit());
}


private PartialResultSet makeBook(String odyssey) {
return PartialResultSet.newBuilder()
.setMetadata(ResultSetMetadata.newBuilder().setRowType(StructType.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,38 @@ void selectWithBackpressureCompletesWhenResultSetEnds() {

}

@Test
void urlConnectionWithExplicitAutocommitOff() {
String baseUrl = String.format(
"r2dbc:spanner://spanner.googleapis.com:443/projects/%s/instances/%s/databases/%s",
ServiceOptions.getDefaultProjectId(),
DatabaseProperties.INSTANCE,
DatabaseProperties.DATABASE);

ConnectionFactory cf = ConnectionFactories.get(baseUrl
+ "?client-implementation=client-library&autocommit=false");
StepVerifier.create(
Mono.from(cf.create()).map(conn -> conn.isAutoCommit())
).expectNext(false)
.verifyComplete();
}

@Test
void urlConnectionWithExplicitReadonlyOn() {
String baseUrl = String.format(
"r2dbc:spanner://spanner.googleapis.com:443/projects/%s/instances/%s/databases/%s",
ServiceOptions.getDefaultProjectId(),
DatabaseProperties.INSTANCE,
DatabaseProperties.DATABASE);

ConnectionFactory cf = ConnectionFactories.get(baseUrl
+ "?client-implementation=client-library&readonly=true");
StepVerifier.create(
Mono.from(cf.create()).map(conn -> ((SpannerConnection) conn).isInReadonlyTransaction())
).expectNext(true)
.verifyComplete();
}

private Publisher<Long> getFirstNumber(Result result) {
return result.map((row, meta) -> (Long) row.get(1));
}
Expand Down
Loading