From 2a95ef61e6a91c98db904bd4ae5b5919d9ff3cb2 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2020 08:32:03 +0000 Subject: [PATCH 01/79] chore: release 2.0.3-SNAPSHOT (#492) :robot: I have created a release \*beep\* \*boop\* --- ### Updating meta-information for bleeding-edge SNAPSHOT release. --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). --- google-cloud-spanner-bom/pom.xml | 18 +++++++++--------- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 16 ++++++++-------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 14 +++++++------- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 2ed27bee666..e6e056633a3 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 2.0.2 + 2.0.3-SNAPSHOT pom com.google.cloud @@ -64,43 +64,43 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.cloud google-cloud-spanner - 2.0.2 + 2.0.3-SNAPSHOT com.google.cloud google-cloud-spanner test-jar - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 326f5e077d5..a0c57fde14f 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 2.0.2 + 2.0.3-SNAPSHOT jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 2.0.2 + 2.0.3-SNAPSHOT google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index e845ba03e14..d6664483ba5 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 2.0.2 + 2.0.3-SNAPSHOT grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 9f51e34edf6..ebeb1bc6f0c 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 2.0.2 + 2.0.3-SNAPSHOT grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index af4bfaad72d..5ac2e93fbfa 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 2.0.2 + 2.0.3-SNAPSHOT grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/pom.xml b/pom.xml index 08df301b8a1..9db4299935c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 2.0.2 + 2.0.3-SNAPSHOT Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -70,37 +70,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 2.0.2 + 2.0.3-SNAPSHOT com.google.cloud google-cloud-spanner - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index d7105cc7b8c..6f72dbabfda 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 2.0.2 + 2.0.3-SNAPSHOT proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 4f7e0889dc9..c62a5297739 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 2.0.2 + 2.0.3-SNAPSHOT proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index fd8dc8dff1a..670d307b11b 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 2.0.2 + 2.0.3-SNAPSHOT proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 39e2fe08dd2..0bce44187ee 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-spanner - 2.0.2 + 2.0.3-SNAPSHOT diff --git a/versions.txt b/versions.txt index fc1a2c984fc..e73ffd448ec 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:2.0.2:2.0.2 -proto-google-cloud-spanner-v1:2.0.2:2.0.2 -proto-google-cloud-spanner-admin-database-v1:2.0.2:2.0.2 -grpc-google-cloud-spanner-v1:2.0.2:2.0.2 -grpc-google-cloud-spanner-admin-instance-v1:2.0.2:2.0.2 -grpc-google-cloud-spanner-admin-database-v1:2.0.2:2.0.2 -google-cloud-spanner:2.0.2:2.0.2 \ No newline at end of file +proto-google-cloud-spanner-admin-instance-v1:2.0.2:2.0.3-SNAPSHOT +proto-google-cloud-spanner-v1:2.0.2:2.0.3-SNAPSHOT +proto-google-cloud-spanner-admin-database-v1:2.0.2:2.0.3-SNAPSHOT +grpc-google-cloud-spanner-v1:2.0.2:2.0.3-SNAPSHOT +grpc-google-cloud-spanner-admin-instance-v1:2.0.2:2.0.3-SNAPSHOT +grpc-google-cloud-spanner-admin-database-v1:2.0.2:2.0.3-SNAPSHOT +google-cloud-spanner:2.0.2:2.0.3-SNAPSHOT \ No newline at end of file From c99294beb43ce1bd67cc3d12e4104641efab6710 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sat, 3 Oct 2020 05:57:08 -0400 Subject: [PATCH 02/79] fix: remove dependency on commons-lang (#494) * remove dependency on commons-lang --- google-cloud-spanner/pom.xml | 7 ------- .../com/google/cloud/spanner/connection/DdlBatch.java | 7 +++++-- .../com/google/cloud/spanner/connection/DdlBatchTest.java | 8 +++----- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index a0c57fde14f..7acd400ec7f 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -237,22 +237,15 @@ com.google.code.findbugs jsr305 - 3.0.2 com.google.code.gson gson - 2.8.6 com.google.auth google-auth-library-credentials - - org.apache.commons - commons-lang3 - 3.5 - diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java index b18f3fa891c..a80e93dfc08 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java @@ -34,12 +34,12 @@ import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import org.apache.commons.lang3.ArrayUtils; /** * {@link UnitOfWork} that is used when a DDL batch is started. These batches only accept DDL @@ -124,7 +124,10 @@ public ResultSet executeQuery( // Queries marked with internal metadata queries are allowed during a DDL batch. // These can only be generated by library internal methods and may be used to check // whether a database object such as table or an index exists. - final QueryOption[] internalOptions = ArrayUtils.remove(options, i); + List temp = new ArrayList<>(); + Collections.addAll(temp, options); + temp.remove(i); + final QueryOption[] internalOptions = temp.toArray(new QueryOption[0]); Callable callable = new Callable() { @Override diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java index 4f02fb9a367..6194b7f73b3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java @@ -156,11 +156,9 @@ public void testExecuteMetadataQuery() { when(singleUse.executeQuery(statement)).thenReturn(resultSet); when(dbClient.singleUse()).thenReturn(singleUse); DdlBatch batch = createSubject(createDefaultMockDdlClient(), dbClient); - assertThat( - batch - .executeQuery(parsedStatement, AnalyzeMode.NONE, InternalMetadataQuery.INSTANCE) - .hashCode(), - is(equalTo(resultSet.hashCode()))); + ResultSet result = + batch.executeQuery(parsedStatement, AnalyzeMode.NONE, InternalMetadataQuery.INSTANCE); + assertThat(result.hashCode(), is(equalTo(resultSet.hashCode()))); } @Test From 9e93098784f8c2757f8fe852ab2bf19cac1252ac Mon Sep 17 00:00:00 2001 From: Les Vogel Date: Tue, 14 Feb 2017 12:15:27 -0800 Subject: [PATCH 03/79] samples: Spanner - Initial commit (#511) --- .../com/example/spanner/QuickstartSample.java | 64 +++ .../com/example/spanner/SpannerSample.java | 507 ++++++++++++++++++ .../example/spanner/QuickstartSampleIT.java | 62 +++ .../com/example/spanner/SpannerSampleIT.java | 112 ++++ 4 files changed, 745 insertions(+) create mode 100644 samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/SpannerSample.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java diff --git a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java new file mode 100644 index 00000000000..eeed92ff811 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java @@ -0,0 +1,64 @@ +/* + Copyright 2017, Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package com.example.spanner; + +// [START spanner_quickstart] +// Imports the Google Cloud client library +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; + +/** + * A quick start code for Cloud Spanner. It demonstrates how to setup the Cloud Spanner client and + * execute a simple query using it against an existing database. + */ +public class QuickstartSample { + public static void main(String... args) throws Exception { + + if (args.length != 2) { + System.err.println("Usage: QuickStartSample "); + return; + } + // Instantiates a client + SpannerOptions options = SpannerOptions.newBuilder().build(); + Spanner spanner = options.getService(); + + // Name of your database. Eg: projects/my-project/instances/instanceId/databases/databaseId + String instanceId = args[0]; + String databaseId = args[1]; + try { + // Creates a database client + DatabaseClient dbClient = spanner.getDatabaseClient(DatabaseId.of( + options.getProjectId(), instanceId, databaseId)); + // Queries the database + ResultSet resultSet = dbClient.singleUse().executeQuery(Statement.of("SELECT 1")); + + System.out.println("\n\nResults:"); + // Prints the results + while (resultSet.next()) { + System.out.printf("%d\n\n", resultSet.getLong(0)); + } + } finally { + // Closes the client which will free up the resources used + spanner.closeAsync().get(); + } + } +} +// [END spanner_quickstart] diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java new file mode 100644 index 00000000000..3d9d9f736d0 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -0,0 +1,507 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START transaction_import] +import static com.google.cloud.spanner.TransactionRunner.TransactionCallable; +// [END transaction_import] + +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +// [START transaction_import] +import com.google.cloud.spanner.Key; +// [END transaction_import] +// [START read_import] +import com.google.cloud.spanner.KeySet; +// [END read_import] +// [START write_import] +import com.google.cloud.spanner.Mutation; +// [END write_import] +import com.google.cloud.spanner.Operation; +// [START read_only_transaction_import] +import com.google.cloud.spanner.ReadOnlyTransaction; +// [END read_only_transaction_import] +// [START query_import] +import com.google.cloud.spanner.ResultSet; +// [END query_import] +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +// [START query_import] +import com.google.cloud.spanner.Statement; +// [END query_import] +// [START transaction_import] +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TransactionContext; +// [END transaction_import] +import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; + +// [START write_import] + +import java.util.ArrayList; +// [END write_import] +import java.util.Arrays; +import java.util.List; + +/** + * Example code for using the Cloud Spanner API. This example demonstrates all the common + * operations that can be done on Cloud Spanner. These are:

+ *

    + *
  • Creating a Cloud Spanner database. + *
  • Writing, reading and executing SQL queries. + *
  • Writing data using a read-write transaction. + *
  • Using an index to read and execute SQL queries over data. + *
+ * + */ +public class SpannerSample { + /** Class to contain singer sample data. */ + static class Singer { + final long singerId; + final String firstName; + final String lastName; + + Singer(long singerId, String firstName, String lastName) { + this.singerId = singerId; + this.firstName = firstName; + this.lastName = lastName; + } + } + + /** Class to contain album sample data. */ + static class Album { + final long singerId; + final long albumId; + final String albumTitle; + + Album(long singerId, long albumId, String albumTitle) { + this.singerId = singerId; + this.albumId = albumId; + this.albumTitle = albumTitle; + } + } + + // [START write] + static final List SINGERS = + Arrays.asList( + new Singer(1, "Marc", "Richards"), + new Singer(2, "Catalina", "Smith"), + new Singer(3, "Alice", "Trentor"), + new Singer(4, "Lea", "Martin"), + new Singer(5, "David", "Lomond")); + + static final List ALBUMS = + Arrays.asList( + new Album(1, 1, "Total Junk"), + new Album(1, 2, "Go, Go, Go"), + new Album(2, 1, "Green"), + new Album(2, 2, "Forever Hold Your Peace"), + new Album(2, 3, "Terrified")); + // [END write] + + static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { + Operation op = dbAdminClient + .createDatabase( + id.getInstanceId().getInstance(), + id.getDatabase(), + Arrays.asList( + "CREATE TABLE Singers (\n" + + " SingerId INT64 NOT NULL,\n" + + " FirstName STRING(1024),\n" + + " LastName STRING(1024),\n" + + " SingerInfo BYTES(MAX)\n" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (\n" + + " SingerId INT64 NOT NULL,\n" + + " AlbumId INT64 NOT NULL,\n" + + " AlbumTitle STRING(MAX)\n" + + ") PRIMARY KEY (SingerId, AlbumId),\n" + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE")); + Database db = op.waitFor().getResult(); + System.out.println("Created database [" + db.getId() + "]"); + } + + // [START write] + static void writeExampleData(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + for (Singer singer : SINGERS) { + mutations.add( + Mutation.newInsertBuilder("Singers") + .set("SingerId") + .to(singer.singerId) + .set("FirstName") + .to(singer.firstName) + .set("LastName") + .to(singer.lastName) + .build()); + } + for (Album album : ALBUMS) { + mutations.add( + Mutation.newInsertBuilder("Albums") + .set("SingerId") + .to(album.singerId) + .set("AlbumId") + .to(album.albumId) + .set("AlbumTitle") + .to(album.albumTitle) + .build()); + } + dbClient.write(mutations); + } + // [END write] + + // [START query] + static void query(DatabaseClient dbClient) { + // singleUse() can be used to execute a single read or query against Cloud Spanner. + ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums")); + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } + } + // [END query] + + // [START read] + static void read(DatabaseClient dbClient) { + ResultSet resultSet = + dbClient + .singleUse() + .read("Albums", + // KeySet.all() can be used to read all rows in a table. KeySet exposes other + // methods to read only a subset of the table. + KeySet.all(), + Arrays.asList("SingerId", "AlbumId", "AlbumTitle")); + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } + } + // [END read] + + // [START add_marketing_budget] + static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseId dbId) { + adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"), + null).waitFor(); + System.out.println("Added MarketingBudget column"); + } + // [END add_marketing_budget] + + // Before executing this method, a new column MarketingBudget has to be added to the Albums + // table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64". + // [START update] + static void update(DatabaseClient dbClient) { + // Mutation can be used to update/insert/delete a single row in a table. Here we use + // newUpdateBuilder to create update mutations. + List mutations = + Arrays.asList( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(1) + .set("AlbumId") + .to(1) + .set("MarketingBudget") + .to(100000) + .build(), + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(2) + .set("AlbumId") + .to(2) + .set("MarketingBudget") + .to(500000) + .build()); + // This writes all the mutations to Cloud Spanner atomically. + dbClient.write(mutations); + } + // [END update] + + // [START transaction] + static void writeWithTransaction(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + // Transfer marketing budget from one album to another. We do it in a transaction to + // ensure that the transfer is atomic. + Struct row = + transaction.readRow("Albums", Key.of(2, 2), Arrays.asList("MarketingBudget")); + long album2Budget = row.getLong(0); + // Transaction will only be committed if this condition still holds at the time of + // commit. Otherwise it will be aborted and the callable will be rerun by the + // client library. + if (album2Budget >= 300000) { + long album1Budget = + transaction + .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget")) + .getLong(0); + long transfer = 200000; + album1Budget += transfer; + album2Budget -= transfer; + transaction.buffer( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(1) + .set("AlbumId") + .to(1) + .set("MarketingBudget") + .to(album1Budget) + .build()); + transaction.buffer( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(2) + .set("AlbumId") + .to(2) + .set("MarketingBudget") + .to(album2Budget) + .build()); + } + return null; + } + }); + } + // [END transaction] + + // [START query_new_column] + static void queryMarketingBudget(DatabaseClient dbClient) { + // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to + // null. + ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums")); + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + } + } + // [END query_new_column] + + // [START add_index] + static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { + adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), + null).waitFor(); + System.out.println("Added AlbumsByAlbumTitle index"); + } + // [END add_index] + + // Before running this example, add the index AlbumsByAlbumTitle by applying the DDL statement + // "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)". + // [START query_index] + static void queryUsingIndex(DatabaseClient dbClient) { + ResultSet resultSet = + dbClient + .singleUse() + .executeQuery( + // We use FORCE_INDEX hint to specify which index to use. For more details see + // https://cloud.google.com/spanner/docs/query-syntax#from-clause + Statement.of( + "SELECT AlbumId, AlbumTitle, MarketingBudget\n" + + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}\n" + + "WHERE AlbumTitle >= 'Aardvark' AND AlbumTitle < 'Goo'")); + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("AlbumId"), + resultSet.getString("AlbumTitle"), + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + } + } + // [END query_index] + + // [START read_index] + static void readUsingIndex(DatabaseClient dbClient) { + ResultSet resultSet = + dbClient + .singleUse() + .readUsingIndex( + "Albums", + "AlbumsByAlbumTitle", + KeySet.all(), + Arrays.asList("AlbumId", "AlbumTitle")); + while (resultSet.next()) { + System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1)); + } + } + // [END read_index] + + // [START add_storing_index] + static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { + adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)"), + null).waitFor(); + System.out.println("Added AlbumsByAlbumTitle2 index"); + } + // [END add_storing_index] + + // Before running this example, create a storing index AlbumsByAlbumTitle2 by applying the DDL + // statement "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)". + // [START read_storing_index] + static void readStoringIndex(DatabaseClient dbClient) { + // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget. + ResultSet resultSet = + dbClient + .singleUse() + .readUsingIndex( + "Albums", + "AlbumsByAlbumTitle2", + KeySet.all(), + Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget")); + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong(0), + resultSet.getString(1), + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + } + } + // [END read_storing_index] + + // [START read_only_transaction] + static void readOnlyTransaction(DatabaseClient dbClient) { + // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it. + // We use a try-with-resource block to automatically do so. + try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) { + ResultSet queryResultSet = + transaction.executeQuery( + Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums")); + while (queryResultSet.next()) { + System.out.printf( + "%d %d %s\n", + queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2)); + } + ResultSet readResultSet = + transaction.read( + "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle")); + while (readResultSet.next()) { + System.out.printf( + "%d %d %s\n", + readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2)); + } + } + } + // [END read_only_transaction] + + static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, String command, + DatabaseId database) { + switch (command) { + case "createdatabase": + createDatabase(dbAdminClient, database); + break; + case "write": + writeExampleData(dbClient); + break; + case "query": + query(dbClient); + break; + case "read": + read(dbClient); + break; + case "addmarketingbudget": + addMarketingBudget(dbAdminClient, database); + break; + case "update": + update(dbClient); + break; + case "writetransaction": + writeWithTransaction(dbClient); + break; + case "querymarketingbudget": + queryMarketingBudget(dbClient); + break; + case "addindex": + addIndex(dbAdminClient, database); + break; + case "readindex": + readUsingIndex(dbClient); + break; + case "queryindex": + queryUsingIndex(dbClient); + break; + case "addstoringindex": + addStoringIndex(dbAdminClient, database); + break; + case "readstoringindex": + readStoringIndex(dbClient); + break; + case "readonlytransaction": + readOnlyTransaction(dbClient); + break; + default: + printUsageAndExit(); + } + } + + static void printUsageAndExit() { + System.err.println("Usage:"); + System.err.println(" SpannerExample "); + System.err.println(""); + System.err.println("Examples:"); + System.err.println( + " SpannerExample createdatabase my-instance example-db"); + System.err.println( + " SpannerExample write my-instance example-db"); + System.exit(1); + } + + public static void main(String[] args) throws Exception { + if (args.length != 3) { + printUsageAndExit(); + } + // [START init_client] + SpannerOptions options = SpannerOptions.newBuilder().build(); + Spanner spanner = options.getService(); + try { + String command = args[0]; + DatabaseId db = DatabaseId.of(options.getProjectId(), args[1], args[2]); + // [END init_client] + // This will return the default project id based on the environment. + String clientProject = spanner.getOptions().getProjectId(); + if (!db.getInstanceId().getProject().equals(clientProject)) { + System.err.println("Invalid project specified. Project in the database id should match" + + "the project name set in the environment variable GCLOUD_PROJECT. Expected: " + + clientProject); + printUsageAndExit(); + } + // [START init_client] + DatabaseClient dbClient = spanner.getDatabaseClient(db); + DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + // [END init_client] + run(dbClient, dbAdminClient, command, db); + } finally { + spanner.closeAsync().get(); + } + System.out.println("Closed client"); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java new file mode 100644 index 00000000000..007fa464f1b --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java @@ -0,0 +1,62 @@ +/* + Copyright 2017, Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package com.example.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * Tests for quickstart sample. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class QuickstartSampleIT { + private String instanceId = System.getProperty("spanner.test.instance"); + // This database needs to exist for test to pass. + private String dbId = System.getProperty("spanner.quickstart.database"); + private ByteArrayOutputStream bout; + private PrintStream out; + + @Before + public void setUp() { + bout = new ByteArrayOutputStream(); + out = new PrintStream(bout); + System.setOut(out); + } + + @After + public void tearDown() { + System.setOut(null); + } + + @Test + public void testQuickstart() throws Exception { + assertThat(instanceId).isNotNull(); + assertThat(dbId).isNotNull(); + QuickstartSample.main(instanceId, dbId); + String got = bout.toString(); + assertThat(got).contains("1"); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java new file mode 100644 index 00000000000..64eb8208071 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * Unit tests for {@code SpannerSample} + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class SpannerSampleIT { + // The instance needs to exist for tests to pass. + String instanceId = System.getProperty("spanner.test.instance"); + String databaseId = System.getProperty("spanner.sample.database"); + DatabaseId dbId; + DatabaseAdminClient dbClient; + + private String runSample(String command) throws Exception { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + SpannerSample.main(new String[]{command, instanceId, databaseId}); + return bout.toString(); + } + + @Before + public void setUp() throws Exception { + SpannerOptions options = SpannerOptions.newBuilder().build(); + Spanner spanner = options.getService(); + dbClient = spanner.getDatabaseAdminClient(); + dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + } + + @After + public void tearDown() throws Exception { + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + } + + @Test + public void testSample() throws Exception { + assertThat(instanceId).isNotNull(); + assertThat(databaseId).isNotNull(); + String out = runSample("createdatabase"); + assertThat(out).contains("Created database"); + assertThat(out).contains(dbId.getName()); + + runSample("write"); + + out = runSample("read"); + assertThat(out).contains("1 1 Total Junk"); + + out = runSample("query"); + assertThat(out).contains("1 1 Total Junk"); + + runSample("addmarketingbudget"); + runSample("update"); + + runSample("writetransaction"); + + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 300000"); + assertThat(out).contains("2 2 300000"); + + runSample("addindex"); + out = runSample("queryindex"); + assertThat(out).contains("Go, Go, Go"); + assertThat(out).contains("Forever Hold Your Peace"); + assertThat(out).doesNotContain("Green"); + + out = runSample("readindex"); + assertThat(out).contains("Go, Go, Go"); + assertThat(out).contains("Forever Hold Your Peace"); + assertThat(out).contains("Green"); + + runSample("addstoringindex"); + out = runSample("readstoringindex"); + assertThat(out).contains("300000"); + + out = runSample("readonlytransaction"); + assertThat(out.replaceAll("[\r\n]+", " ")).containsMatch("(Total Junk.*){2}"); + } +} From 08960f09d8eaa1ceb07939da3fde2c6e4534bb2c Mon Sep 17 00:00:00 2001 From: Les Vogel Date: Thu, 16 Feb 2017 21:55:26 -0800 Subject: [PATCH 04/79] samples: Get full IT working again (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Spanner Works 1. remove comments from pom.xml 2. explicitly pass the SystemPropertyValues in local pom — not sure how it worked earlier. 3. Fix a comment in the QuickStart sample 4. Restore stdOut in both IT samples * Minor README tweak storage-transfer * ErrorProne 1. Turn on ErrorProne - `mvn clean verify -DskipTests` passes 2. Fixes for ErrorProne - mostly removing compiler plugin, java 1.8 for most samples. (No more Java 5) 3. Contributing now mentions gradle and testing 4. Fix storage-transfer README 5. back out some changes to MAVEN_OPTS * Debugging Magic 1. What project are these tests being run on? 2. Skip spanner for now. * change project_ID 1. enabled cloud-samples-tests 2. change the project 3. remove the maven memory hack. 4. remove the debugging code. * revert to j-d-s-t for projectID that BQ uses --- .../src/main/java/com/example/spanner/QuickstartSample.java | 2 +- .../test/java/com/example/spanner/QuickstartSampleIT.java | 3 ++- .../src/test/java/com/example/spanner/SpannerSampleIT.java | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java index eeed92ff811..6b0fdc69c8c 100644 --- a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java @@ -40,7 +40,7 @@ public static void main(String... args) throws Exception { SpannerOptions options = SpannerOptions.newBuilder().build(); Spanner spanner = options.getService(); - // Name of your database. Eg: projects/my-project/instances/instanceId/databases/databaseId + // Name of your instance & database. String instanceId = args[0]; String databaseId = args[1]; try { diff --git a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java index 007fa464f1b..15df8e810fe 100644 --- a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java @@ -37,6 +37,7 @@ public class QuickstartSampleIT { // This database needs to exist for test to pass. private String dbId = System.getProperty("spanner.quickstart.database"); private ByteArrayOutputStream bout; + private PrintStream stdOut = System.out; private PrintStream out; @Before @@ -48,7 +49,7 @@ public void setUp() { @After public void tearDown() { - System.setOut(null); + System.setOut(stdOut); } @Test diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 64eb8208071..fa1b9939a4d 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -39,16 +39,18 @@ @SuppressWarnings("checkstyle:abbreviationaswordinname") public class SpannerSampleIT { // The instance needs to exist for tests to pass. - String instanceId = System.getProperty("spanner.test.instance"); - String databaseId = System.getProperty("spanner.sample.database"); + private final String instanceId = System.getProperty("spanner.test.instance"); + private final String databaseId = System.getProperty("spanner.sample.database"); DatabaseId dbId; DatabaseAdminClient dbClient; private String runSample(String command) throws Exception { + PrintStream stdOut = System.out; ByteArrayOutputStream bout = new ByteArrayOutputStream(); PrintStream out = new PrintStream(bout); System.setOut(out); SpannerSample.main(new String[]{command, instanceId, databaseId}); + System.setOut(stdOut); return bout.toString(); } From bcc25c443e0951f48b335ba3780f2b5d76bab8ad Mon Sep 17 00:00:00 2001 From: DPE bot Date: Mon, 1 May 2017 16:14:30 -0700 Subject: [PATCH 05/79] samples: Auto-update dependencies. (#636) * Auto-update dependencies. * Roll back speech & Vision due to weird GAX issues. Fix for GA Datastore, Storage, and more. * Fix pom.xml Downgrade to prior version. --- .../src/main/java/com/example/spanner/QuickstartSample.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java index 6b0fdc69c8c..8904edcbd4c 100644 --- a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java @@ -57,7 +57,7 @@ public static void main(String... args) throws Exception { } } finally { // Closes the client which will free up the resources used - spanner.closeAsync().get(); + spanner.closeAsync(); } } } From 2aabd8c2898db2bac10ef6e52f2ffd8f3d4575cc Mon Sep 17 00:00:00 2001 From: Jisha Abubaker Date: Mon, 19 Jun 2017 16:22:12 -0700 Subject: [PATCH 06/79] samples: updating to latest google-cloud-* dependencies (#723) --- .../src/main/java/com/example/spanner/QuickstartSample.java | 2 +- .../src/main/java/com/example/spanner/SpannerSample.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java index 8904edcbd4c..a05c847e3ed 100644 --- a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java @@ -57,7 +57,7 @@ public static void main(String... args) throws Exception { } } finally { // Closes the client which will free up the resources used - spanner.closeAsync(); + spanner.close(); } } } diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 3d9d9f736d0..176677b44f0 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -500,7 +500,7 @@ public static void main(String[] args) throws Exception { // [END init_client] run(dbClient, dbAdminClient, command, db); } finally { - spanner.closeAsync().get(); + spanner.close(); } System.out.println("Closed client"); } From ea653f70330811b9de6bb8d4b8340666c2c7a904 Mon Sep 17 00:00:00 2001 From: Jisha Abubaker Date: Mon, 28 Aug 2017 10:39:27 -0700 Subject: [PATCH 07/79] samples: Spanner stale read (#831) --- .../com/example/spanner/SpannerSample.java | 85 ++++++++++--------- .../example/spanner/QuickstartSampleIT.java | 5 +- .../com/example/spanner/SpannerSampleIT.java | 17 ++-- 3 files changed, 59 insertions(+), 48 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 176677b44f0..7b83efb1d9d 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -16,47 +16,29 @@ package com.example.spanner; -// [START transaction_import] import static com.google.cloud.spanner.TransactionRunner.TransactionCallable; -// [END transaction_import] import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.DatabaseId; -// [START transaction_import] import com.google.cloud.spanner.Key; -// [END transaction_import] -// [START read_import] import com.google.cloud.spanner.KeySet; -// [END read_import] -// [START write_import] import com.google.cloud.spanner.Mutation; -// [END write_import] import com.google.cloud.spanner.Operation; -// [START read_only_transaction_import] import com.google.cloud.spanner.ReadOnlyTransaction; -// [END read_only_transaction_import] -// [START query_import] import com.google.cloud.spanner.ResultSet; -// [END query_import] import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; -// [START query_import] import com.google.cloud.spanner.Statement; -// [END query_import] -// [START transaction_import] import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.TransactionContext; -// [END transaction_import] import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; - -// [START write_import] - import java.util.ArrayList; -// [END write_import] import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; /** * Example code for using the Cloud Spanner API. This example demonstrates all the common @@ -67,11 +49,14 @@ *
  • Writing data using a read-write transaction. *
  • Using an index to read and execute SQL queries over data. * - * */ public class SpannerSample { - /** Class to contain singer sample data. */ + + /** + * Class to contain singer sample data. + */ static class Singer { + final long singerId; final String firstName; final String lastName; @@ -83,8 +68,11 @@ static class Singer { } } - /** Class to contain album sample data. */ + /** + * Class to contain album sample data. + */ static class Album { + final long singerId; final long albumId; final String albumTitle; @@ -116,22 +104,22 @@ static class Album { static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { Operation op = dbAdminClient - .createDatabase( - id.getInstanceId().getInstance(), - id.getDatabase(), - Arrays.asList( - "CREATE TABLE Singers (\n" - + " SingerId INT64 NOT NULL,\n" - + " FirstName STRING(1024),\n" - + " LastName STRING(1024),\n" - + " SingerInfo BYTES(MAX)\n" - + ") PRIMARY KEY (SingerId)", - "CREATE TABLE Albums (\n" - + " SingerId INT64 NOT NULL,\n" - + " AlbumId INT64 NOT NULL,\n" - + " AlbumTitle STRING(MAX)\n" - + ") PRIMARY KEY (SingerId, AlbumId),\n" - + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE")); + .createDatabase( + id.getInstanceId().getInstance(), + id.getDatabase(), + Arrays.asList( + "CREATE TABLE Singers (\n" + + " SingerId INT64 NOT NULL,\n" + + " FirstName STRING(1024),\n" + + " LastName STRING(1024),\n" + + " SingerInfo BYTES(MAX)\n" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (\n" + + " SingerId INT64 NOT NULL,\n" + + " AlbumId INT64 NOT NULL,\n" + + " AlbumTitle STRING(MAX)\n" + + ") PRIMARY KEY (SingerId, AlbumId),\n" + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE")); Database db = op.waitFor().getResult(); System.out.println("Created database [" + db.getId() + "]"); } @@ -413,6 +401,22 @@ static void readOnlyTransaction(DatabaseClient dbClient) { } // [END read_only_transaction] + // [START read_stale_data] + static void readStaleData(DatabaseClient dbClient) { + ResultSet resultSet = + dbClient + .singleUse(TimestampBound.ofExactStaleness(10, TimeUnit.SECONDS)) + .read("Albums", + KeySet.all(), + Arrays.asList("SingerId", "AlbumId", "MarketingBudget")); + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), + resultSet.isNull(2) ? "NULL" : resultSet.getLong("MarketingBudget")); + } + } + // [END read_stale_data] + static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, String command, DatabaseId database) { switch (command) { @@ -458,6 +462,9 @@ static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, Stri case "readonlytransaction": readOnlyTransaction(dbClient); break; + case "readstaledata": + readStaleData(dbClient); + break; default: printUsageAndExit(); } diff --git a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java index 15df8e810fe..350f2b3d9a1 100644 --- a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java @@ -18,15 +18,14 @@ import static com.google.common.truth.Truth.assertThat; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; - /** * Tests for quickstart sample. */ diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index fa1b9939a4d..fd695426b84 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -23,15 +23,14 @@ import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; - /** * Unit tests for {@code SpannerSample} */ @@ -43,6 +42,7 @@ public class SpannerSampleIT { private final String databaseId = System.getProperty("spanner.sample.database"); DatabaseId dbId; DatabaseAdminClient dbClient; + private long lastUpdateDataTimeInMillis; private String runSample(String command) throws Exception { PrintStream stdOut = System.out; @@ -83,12 +83,17 @@ public void testSample() throws Exception { out = runSample("query"); assertThat(out).contains("1 1 Total Junk"); - runSample("addmarketingbudget"); + + // wait for 10 seconds to elapse and then run an update, and query for stale data + lastUpdateDataTimeInMillis = System.currentTimeMillis(); + while (System.currentTimeMillis() < lastUpdateDataTimeInMillis + 11000) { + Thread.sleep(1000); + } runSample("update"); - + out = runSample("readstaledata"); + assertThat(out).contains("1 1 NULL"); runSample("writetransaction"); - out = runSample("querymarketingbudget"); assertThat(out).contains("1 1 300000"); assertThat(out).contains("2 2 300000"); From fd092d06c14bff552d0a94cbfeeccb0fe76dd77f Mon Sep 17 00:00:00 2001 From: Jisha Abubaker Date: Tue, 26 Sep 2017 13:12:18 -0700 Subject: [PATCH 08/79] samples: adding a test prefix to db to allow for concurrent runs (#867) --- .../src/test/java/com/example/spanner/SpannerSampleIT.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index fd695426b84..3a8c3e59033 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -25,6 +25,7 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.util.UUID; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -39,7 +40,7 @@ public class SpannerSampleIT { // The instance needs to exist for tests to pass. private final String instanceId = System.getProperty("spanner.test.instance"); - private final String databaseId = System.getProperty("spanner.sample.database"); + private final String databaseId = formatForTest(System.getProperty("spanner.sample.database")); DatabaseId dbId; DatabaseAdminClient dbClient; private long lastUpdateDataTimeInMillis; @@ -116,4 +117,8 @@ public void testSample() throws Exception { out = runSample("readonlytransaction"); assertThat(out.replaceAll("[\r\n]+", " ")).containsMatch("(Total Junk.*){2}"); } + + private String formatForTest(String name) { + return name + "-" + UUID.randomUUID().toString().substring(0, 20); + } } From fa22d61dc394f3d2f066075e192b5e309d6218df Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Date: Tue, 7 Nov 2017 11:03:08 -0800 Subject: [PATCH 09/79] samples: Updated SpannerSample to highlight bound parameters. (#910) --- .../com/example/spanner/SpannerSample.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 7b83efb1d9d..bae2b2d6965 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -306,16 +306,19 @@ static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { // "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)". // [START query_index] static void queryUsingIndex(DatabaseClient dbClient) { - ResultSet resultSet = - dbClient - .singleUse() - .executeQuery( - // We use FORCE_INDEX hint to specify which index to use. For more details see - // https://cloud.google.com/spanner/docs/query-syntax#from-clause - Statement.of( - "SELECT AlbumId, AlbumTitle, MarketingBudget\n" - + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}\n" - + "WHERE AlbumTitle >= 'Aardvark' AND AlbumTitle < 'Goo'")); + Statement statement = Statement + // We use FORCE_INDEX hint to specify which index to use. For more details see + // https://cloud.google.com/spanner/docs/query-syntax#from-clause + .newBuilder("SELECT AlbumId, AlbumTitle, MarketingBudget\n" + + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}\n" + + "WHERE AlbumTitle >= @StartTitle AND AlbumTitle < @EndTitle") + // We use @BoundParameters to help speed up frequently executed queries. + // For more details see https://cloud.google.com/spanner/docs/sql-best-practices + .bind("StartTitle").to("Aardvark") + .bind("EndTitle").to("Goo") + .build(); + + ResultSet resultSet = dbClient.singleUse().executeQuery(statement); while (resultSet.next()) { System.out.printf( "%d %s %s\n", From 31483928fb715363ef1de3f33a54aab98096378f Mon Sep 17 00:00:00 2001 From: Jeffrey Rennie Date: Wed, 8 Nov 2017 17:05:07 -0800 Subject: [PATCH 10/79] samples: Bump spanner stale read from 10 to 15 seconds. (#915) At the request of the spanner team. --- .../src/main/java/com/example/spanner/SpannerSample.java | 2 +- .../src/test/java/com/example/spanner/SpannerSampleIT.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index bae2b2d6965..43a516f4bfc 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -408,7 +408,7 @@ static void readOnlyTransaction(DatabaseClient dbClient) { static void readStaleData(DatabaseClient dbClient) { ResultSet resultSet = dbClient - .singleUse(TimestampBound.ofExactStaleness(10, TimeUnit.SECONDS)) + .singleUse(TimestampBound.ofExactStaleness(15, TimeUnit.SECONDS)) .read("Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "MarketingBudget")); diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 3a8c3e59033..79242f7dcbe 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -86,9 +86,9 @@ public void testSample() throws Exception { assertThat(out).contains("1 1 Total Junk"); runSample("addmarketingbudget"); - // wait for 10 seconds to elapse and then run an update, and query for stale data + // wait for 15 seconds to elapse and then run an update, and query for stale data lastUpdateDataTimeInMillis = System.currentTimeMillis(); - while (System.currentTimeMillis() < lastUpdateDataTimeInMillis + 11000) { + while (System.currentTimeMillis() < lastUpdateDataTimeInMillis + 16000) { Thread.sleep(1000); } runSample("update"); From 2faf1a504069969eb134bd0d00d452bfe25a52f2 Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Date: Thu, 18 Jan 2018 08:18:34 -0800 Subject: [PATCH 11/79] samples: Updated mlengine, monitoring, pubsub, spanner, and speech. (#993) * Updated mlengine/online-prediction. * Updated monitoring/ * Updated pubsub/ * Updated spanner/ * Updated speech/ * Fixed spanner mistakes. --- .../com/example/spanner/QuickstartSample.java | 28 +++++++++---------- .../com/example/spanner/SpannerSample.java | 2 +- .../example/spanner/QuickstartSampleIT.java | 28 +++++++++---------- .../com/example/spanner/SpannerSampleIT.java | 2 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java index a05c847e3ed..eaa64ed62aa 100644 --- a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java @@ -1,18 +1,18 @@ /* - Copyright 2017, Google, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.example.spanner; diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 43a516f4bfc..f20c1b8a110 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java index 350f2b3d9a1..707018d71c6 100644 --- a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java @@ -1,18 +1,18 @@ /* - Copyright 2017, Google, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.example.spanner; diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 79242f7dcbe..8f6a2e7457d 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, From 1e5fe08cbaddd88d48f85856dcf6fa3e71b3311c Mon Sep 17 00:00:00 2001 From: Vikas Kedia Date: Tue, 23 Jan 2018 13:13:31 -0800 Subject: [PATCH 12/79] samples: Adds tracing sample for cloud spanner (#1002) * Add tracing * Fixes * tracing * Add tracing sample * Revert Quickstart * Revert Spanner sample * Address review comments * Fix style violations --- .../com/example/spanner/TracingSample.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 samples/snippets/src/main/java/com/example/spanner/TracingSample.java diff --git a/samples/snippets/src/main/java/com/example/spanner/TracingSample.java b/samples/snippets/src/main/java/com/example/spanner/TracingSample.java new file mode 100644 index 00000000000..4dfd5a1cb15 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/TracingSample.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; + +import io.opencensus.common.Scope; +import io.opencensus.contrib.grpc.metrics.RpcViews; +import io.opencensus.contrib.zpages.ZPageHandlers; +import io.opencensus.exporter.stats.stackdriver.StackdriverStatsExporter; +import io.opencensus.exporter.trace.stackdriver.StackdriverExporter; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.samplers.Samplers; + +import java.util.Arrays; + +/** + * This sample demonstrates how to enable opencensus tracing and stats in cloud spanner client. + */ +public class TracingSample { + + private static final String SAMPLE_SPAN = "CloudSpannerSample"; + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + System.err.println("Usage: TracingSample "); + return; + } + SpannerOptions options = SpannerOptions.newBuilder().build(); + Spanner spanner = options.getService(); + + // Installs a handler for /tracez page. + ZPageHandlers.startHttpServerAndRegisterAll(8080); + // Installs an exporter for stack driver traces. + StackdriverExporter.createAndRegister(); + Tracing.getExportComponent().getSampledSpanStore().registerSpanNamesForCollection( + Arrays.asList(SAMPLE_SPAN)); + + // Installs an exporter for stack driver stats. + StackdriverStatsExporter.createAndRegister(); + RpcViews.registerAllCumulativeViews(); + + // Name of your instance & database. + String instanceId = args[0]; + String databaseId = args[1]; + try { + // Creates a database client + DatabaseClient dbClient = spanner.getDatabaseClient(DatabaseId.of( + options.getProjectId(), instanceId, databaseId)); + // Queries the database + try (Scope ss = Tracing.getTracer() + .spanBuilderWithExplicitParent(SAMPLE_SPAN, null) + .setSampler(Samplers.alwaysSample()) + .startScopedSpan()) { + ResultSet resultSet = dbClient.singleUse().executeQuery(Statement.of("SELECT 1")); + + System.out.println("\n\nResults:"); + // Prints the results + while (resultSet.next()) { + System.out.printf("%d\n\n", resultSet.getLong(0)); + } + } + } finally { + // Closes the client which will free up the resources used + spanner.close(); + } + } + +} From 982875dc9bdfcbcd03b7cb6a7b9f0b91791e7643 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Thu, 1 Mar 2018 10:07:05 -0800 Subject: [PATCH 13/79] samples: Clean up Spanner region tags. (#1045) --- .../com/example/spanner/SpannerSample.java | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index f20c1b8a110..b6a4283ff68 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -84,7 +84,7 @@ static class Album { } } - // [START write] + // [START spanner_insert_data] static final List SINGERS = Arrays.asList( new Singer(1, "Marc", "Richards"), @@ -100,8 +100,9 @@ static class Album { new Album(2, 1, "Green"), new Album(2, 2, "Forever Hold Your Peace"), new Album(2, 3, "Terrified")); - // [END write] + // [END spanner_insert_data] + // [START spanner_create_database] static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { Operation op = dbAdminClient .createDatabase( @@ -123,8 +124,9 @@ static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { Database db = op.waitFor().getResult(); System.out.println("Created database [" + db.getId() + "]"); } + // [END spanner_create_database] - // [START write] + // [START spanner_insert_data] static void writeExampleData(DatabaseClient dbClient) { List mutations = new ArrayList<>(); for (Singer singer : SINGERS) { @@ -151,9 +153,9 @@ static void writeExampleData(DatabaseClient dbClient) { } dbClient.write(mutations); } - // [END write] + // [END spanner_insert_data] - // [START query] + // [START spanner_query_data] static void query(DatabaseClient dbClient) { // singleUse() can be used to execute a single read or query against Cloud Spanner. ResultSet resultSet = @@ -165,9 +167,9 @@ static void query(DatabaseClient dbClient) { "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); } } - // [END query] + // [END spanner_query_data] - // [START read] + // [START spanner_read_data] static void read(DatabaseClient dbClient) { ResultSet resultSet = dbClient @@ -182,9 +184,9 @@ static void read(DatabaseClient dbClient) { "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); } } - // [END read] + // [END spanner_read_data] - // [START add_marketing_budget] + // [START spanner_add_column] static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseId dbId) { adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), dbId.getDatabase(), @@ -192,11 +194,11 @@ static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseId dbId) null).waitFor(); System.out.println("Added MarketingBudget column"); } - // [END add_marketing_budget] + // [END spanner_add_column] // Before executing this method, a new column MarketingBudget has to be added to the Albums // table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64". - // [START update] + // [START spanner_update_data] static void update(DatabaseClient dbClient) { // Mutation can be used to update/insert/delete a single row in a table. Here we use // newUpdateBuilder to create update mutations. @@ -221,9 +223,9 @@ static void update(DatabaseClient dbClient) { // This writes all the mutations to Cloud Spanner atomically. dbClient.write(mutations); } - // [END update] + // [END spanner_update_data] - // [START transaction] + // [START spanner_read_write_transaction] static void writeWithTransaction(DatabaseClient dbClient) { dbClient .readWriteTransaction() @@ -270,9 +272,9 @@ public Void run(TransactionContext transaction) throws Exception { } }); } - // [END transaction] + // [END spanner_read_write_transaction] - // [START query_new_column] + // [START spanner_query_with_new_column] static void queryMarketingBudget(DatabaseClient dbClient) { // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to // null. @@ -290,9 +292,9 @@ static void queryMarketingBudget(DatabaseClient dbClient) { resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); } } - // [END query_new_column] + // [END spanner_query_with_new_column] - // [START add_index] + // [START spanner_create_index] static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), dbId.getDatabase(), @@ -300,11 +302,11 @@ static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { null).waitFor(); System.out.println("Added AlbumsByAlbumTitle index"); } - // [END add_index] + // [END spanner_create_index] // Before running this example, add the index AlbumsByAlbumTitle by applying the DDL statement // "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)". - // [START query_index] + // [START spanner_query_data_with_index] static void queryUsingIndex(DatabaseClient dbClient) { Statement statement = Statement // We use FORCE_INDEX hint to specify which index to use. For more details see @@ -327,9 +329,9 @@ static void queryUsingIndex(DatabaseClient dbClient) { resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); } } - // [END query_index] + // [END spanner_query_data_with_index] - // [START read_index] + // [START spanner_read_data_with_index] static void readUsingIndex(DatabaseClient dbClient) { ResultSet resultSet = dbClient @@ -343,9 +345,9 @@ static void readUsingIndex(DatabaseClient dbClient) { System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1)); } } - // [END read_index] + // [END spanner_read_data_with_index] - // [START add_storing_index] + // [START spanner_create_storing_index] static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), dbId.getDatabase(), @@ -354,11 +356,11 @@ static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { null).waitFor(); System.out.println("Added AlbumsByAlbumTitle2 index"); } - // [END add_storing_index] + // [END spanner_create_storing_index] // Before running this example, create a storing index AlbumsByAlbumTitle2 by applying the DDL // statement "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)". - // [START read_storing_index] + // [START spanner_read_data_with_storing_index] static void readStoringIndex(DatabaseClient dbClient) { // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget. ResultSet resultSet = @@ -377,9 +379,9 @@ static void readStoringIndex(DatabaseClient dbClient) { resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); } } - // [END read_storing_index] + // [END spanner_read_data_with_storing_index] - // [START read_only_transaction] + // [START spanner_read_only_transaction] static void readOnlyTransaction(DatabaseClient dbClient) { // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it. // We use a try-with-resource block to automatically do so. @@ -402,9 +404,9 @@ static void readOnlyTransaction(DatabaseClient dbClient) { } } } - // [END read_only_transaction] + // [END spanner_read_only_transaction] - // [START read_stale_data] + // [START spanner_read_stale_data] static void readStaleData(DatabaseClient dbClient) { ResultSet resultSet = dbClient @@ -418,7 +420,7 @@ static void readStaleData(DatabaseClient dbClient) { resultSet.isNull(2) ? "NULL" : resultSet.getLong("MarketingBudget")); } } - // [END read_stale_data] + // [END spanner_read_stale_data] static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, String command, DatabaseId database) { From 760fc8a744379de2a88e64c3975c2e99c5aaa27d Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Thu, 1 Mar 2018 10:40:37 -0800 Subject: [PATCH 14/79] samples: Add Spanner BatchQuery sample. (#1043) * Add Spanner BatchQuery sample. * Update pom.xml to use latest client library. * Update BatchSample.java --- .../java/com/example/spanner/BatchSample.java | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 samples/snippets/src/main/java/com/example/spanner/BatchSample.java diff --git a/samples/snippets/src/main/java/com/example/spanner/BatchSample.java b/samples/snippets/src/main/java/com/example/spanner/BatchSample.java new file mode 100644 index 00000000000..1f44be61ae0 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/BatchSample.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import com.google.cloud.spanner.BatchClient; +import com.google.cloud.spanner.BatchReadOnlyTransaction; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Partition; +import com.google.cloud.spanner.PartitionOptions; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.TimestampBound; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Sample showing how to run a query using the Batch API. + */ +public class BatchSample { + + /** + * This example showcases how to create a batch client, partition a query, and concurrently read + * from multiple partitions. + */ + public static void main(String[] args) throws InterruptedException { + if (args.length != 2) { + System.err.println("Usage: BatchSample "); + return; + } + + /* + * CREATE TABLE Singers ( + * SingerId INT64 NOT NULL, + * FirstName STRING(1024), + * LastName STRING(1024), + * SingerInfo BYTES(MAX), + * ) PRIMARY KEY (SingerId); + */ + + String instanceId = args[0]; + String databaseId = args[1]; + + SpannerOptions options = SpannerOptions.newBuilder().build(); + Spanner spanner = options.getService(); + + // [START spanner_batch_client] + int numThreads = Runtime.getRuntime().availableProcessors(); + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + + // Statistics + int totalPartitions; + AtomicInteger totalRecords = new AtomicInteger(0); + + try { + BatchClient batchClient = spanner.getBatchClient( + DatabaseId.of(options.getProjectId(), instanceId, databaseId)); + + final BatchReadOnlyTransaction txn = + batchClient.batchReadOnlyTransaction(TimestampBound.strong()); + + // A Partition object is serializable and can be used from a different process. + List partitions = txn.partitionQuery(PartitionOptions.getDefaultInstance(), + Statement.of("SELECT SingerId, FirstName, LastName FROM Singers")); + + totalPartitions = partitions.size(); + + for (final Partition p : partitions) { + executor.execute(() -> { + try (ResultSet results = txn.execute(p)) { + while (results.next()) { + long singerId = results.getLong(0); + String firstName = results.getString(1); + String lastName = results.getString(2); + System.out.println("[" + singerId + "] " + firstName + " " + lastName); + totalRecords.getAndIncrement(); + } + } + }); + } + } finally { + executor.shutdown(); + executor.awaitTermination(1, TimeUnit.HOURS); + spanner.close(); + } + + double avgRecordsPerPartition = 0.0; + if (totalPartitions != 0) { + avgRecordsPerPartition = (double) totalRecords.get() / totalPartitions; + } + System.out.println("totalPartitions=" + totalPartitions); + System.out.println("totalRecords=" + totalRecords); + System.out.println("avgRecordsPerPartition=" + avgRecordsPerPartition); + // [END spanner_batch_client] + } +} From 61dbd7dc6c84b0dc424ca9bdde76c24facc1938f Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Thu, 1 Mar 2018 15:32:11 -0800 Subject: [PATCH 15/79] samples: Fix a Spanner region tag. (#1047) --- .../src/main/java/com/example/spanner/SpannerSample.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index b6a4283ff68..1ad6c4dfa68 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -274,7 +274,7 @@ public Void run(TransactionContext transaction) throws Exception { } // [END spanner_read_write_transaction] - // [START spanner_query_with_new_column] + // [START spanner_query_data_with_new_column] static void queryMarketingBudget(DatabaseClient dbClient) { // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to // null. @@ -292,7 +292,7 @@ static void queryMarketingBudget(DatabaseClient dbClient) { resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); } } - // [END spanner_query_with_new_column] + // [END spanner_query_data_with_new_column] // [START spanner_create_index] static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { From abfa69b246312ce51d1b6dfd81f4b023a451a2f0 Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Wed, 28 Mar 2018 13:26:09 -0700 Subject: [PATCH 16/79] samples: Add Spanner commit timestamp sample. (#1072) * Add Spanner commit timestamp sample. * Update pom file to use latest version. --- .../com/example/spanner/SpannerSample.java | 219 ++++++++++++++++++ .../com/example/spanner/SpannerSampleIT.java | 19 +- 2 files changed, 237 insertions(+), 1 deletion(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 1ad6c4dfa68..51133ca4d62 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -34,7 +34,9 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.TransactionContext; +import com.google.cloud.spanner.Value; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -48,6 +50,7 @@ *
  • Writing, reading and executing SQL queries. *
  • Writing data using a read-write transaction. *
  • Using an index to read and execute SQL queries over data. + *
  • Using commit timestamp for tracking when a record was last updated. * */ public class SpannerSample { @@ -84,6 +87,24 @@ static class Album { } } + /** + * Class to contain performance sample data. + */ + static class Performance { + + final long singerId; + final long venueId; + final String eventDate; + final long revenue; + + Performance(long singerId, long venueId, String eventDate, long revenue) { + this.singerId = singerId; + this.venueId = venueId; + this.eventDate = eventDate; + this.revenue = revenue; + } + } + // [START spanner_insert_data] static final List SINGERS = Arrays.asList( @@ -102,6 +123,14 @@ static class Album { new Album(2, 3, "Terrified")); // [END spanner_insert_data] + // [START spanner_insert_data_with_timestamp_column] + static final List PERFORMANCES = + Arrays.asList( + new Performance(1, 4, "2017-10-05", 11000), + new Performance(1, 19, "2017-11-02", 15000), + new Performance(2, 42, "2017-12-23", 7000)); + // [END spanner_insert_data_with_timestamp_column] + // [START spanner_create_database] static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { Operation op = dbAdminClient @@ -126,6 +155,48 @@ static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { } // [END spanner_create_database] + // [START spanner_create_table_with_timestamp_column] + static void createTableWithTimestamp(DatabaseAdminClient dbAdminClient, DatabaseId id) { + Operation op = dbAdminClient + .updateDatabaseDdl( + id.getInstanceId().getInstance(), + id.getDatabase(), + Arrays.asList( + "CREATE TABLE Performances (\n" + + " SingerId INT64 NOT NULL,\n" + + " VenueId INT64 NOT NULL,\n" + + " EventDate Date,\n" + + " Revenue INT64, \n" + + " LastUpdateTime TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)\n" + + ") PRIMARY KEY (SingerId, VenueId, EventDate),\n" + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), null); + op.waitFor().getResult(); + System.out.println("Created Performances table in database: [" + id + "]"); + } + // [END spanner_create_table_with_timestamp_column] + + // [START spanner_insert_data_with_timestamp_column] + static void writeExampleDataWithTimestamp(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + for (Performance performance : PERFORMANCES) { + mutations.add( + Mutation.newInsertBuilder("Performances") + .set("SingerId") + .to(performance.singerId) + .set("VenueId") + .to(performance.venueId) + .set("EventDate") + .to(performance.eventDate) + .set("Revenue") + .to(performance.revenue) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) + .build()); + } + dbClient.write(mutations); + } + // [END spanner_insert_data_with_timestamp_column] + // [START spanner_insert_data] static void writeExampleData(DatabaseClient dbClient) { List mutations = new ArrayList<>(); @@ -422,6 +493,98 @@ static void readStaleData(DatabaseClient dbClient) { } // [END spanner_read_stale_data] + // [START spanner_add_timestamp_column] + static void addCommitTimestamp(DatabaseAdminClient adminClient, DatabaseId dbId) { + adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " + + "OPTIONS (allow_commit_timestamp=true)"), + null).waitFor(); + System.out.println("Added LastUpdateTime as a commit timestamp column in Albums table."); + } + // [END spanner_add_timestamp_column] + + // Before executing this method, a new column MarketingBudget has to be added to the Albums + // table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64". + // In addition this update expects the LastUpdateTime column added by applying the DDL statement + // "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP OPTIONS (allow_commit_timestamp=true)" + // [START spanner_update_data_with_timestamp_column] + static void updateWithTimestamp(DatabaseClient dbClient) { + // Mutation can be used to update/insert/delete a single row in a table. Here we use + // newUpdateBuilder to create update mutations. + List mutations = + Arrays.asList( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(1) + .set("AlbumId") + .to(1) + .set("MarketingBudget") + .to(1000000) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) + .build(), + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(2) + .set("AlbumId") + .to(2) + .set("MarketingBudget") + .to(750000) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) + .build()); + // This writes all the mutations to Cloud Spanner atomically. + dbClient.write(mutations); + } + // [END spanner_update_data_with_timestamp_column] + + // [START spanner_query_data_with_timestamp_column] + static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { + // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to + // null. + ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of( + "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime FROM Albums" + + " ORDER BY LastUpdateTime DESC")); + while (resultSet.next()) { + System.out.printf( + "%d %d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"), + resultSet.isNull("LastUpdateTime") ? "NULL" : resultSet.getTimestamp("LastUpdateTime")); + } + } + // [END spanner_query_data_with_timestamp_column] + + static void queryPerformancesTable(DatabaseClient dbClient) { + // Rows without an explicit value for Revenue will have a Revenue equal to + // null. + ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of( + "SELECT SingerId, VenueId, EventDate, Revenue, LastUpdateTime FROM Performances" + + " ORDER BY LastUpdateTime DESC")); + while (resultSet.next()) { + System.out.printf( + "%d %d %s %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("VenueId"), + resultSet.getDate("EventDate"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("Revenue") ? "NULL" : resultSet.getLong("Revenue"), + resultSet.getTimestamp("LastUpdateTime")); + } + } + static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, String command, DatabaseId database) { switch (command) { @@ -470,6 +633,24 @@ static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, Stri case "readstaledata": readStaleData(dbClient); break; + case "addcommittimestamp": + addCommitTimestamp(dbAdminClient, database); + break; + case "updatewithtimestamp": + updateWithTimestamp(dbClient); + break; + case "querywithtimestamp": + queryMarketingBudgetWithTimestamp(dbClient); + break; + case "createtablewithtimestamp": + createTableWithTimestamp(dbAdminClient, database); + break; + case "writewithtimestamp": + writeExampleDataWithTimestamp(dbClient); + break; + case "queryperformancestable": + queryPerformancesTable(dbClient); + break; default: printUsageAndExit(); } @@ -484,6 +665,44 @@ static void printUsageAndExit() { " SpannerExample createdatabase my-instance example-db"); System.err.println( " SpannerExample write my-instance example-db"); + System.err.println( + " SpannerExample query my-instance example-db"); + System.err.println( + " SpannerExample read my-instance example-db"); + System.err.println( + " SpannerExample addmarketingbudget my-instance example-db"); + System.err.println( + " SpannerExample update my-instance example-db"); + System.err.println( + " SpannerExample writetransaction my-instance example-db"); + System.err.println( + " SpannerExample querymarketingbudget my-instance example-db"); + System.err.println( + " SpannerExample addindex my-instance example-db"); + System.err.println( + " SpannerExample readindex my-instance example-db"); + System.err.println( + " SpannerExample queryindex my-instance example-db"); + System.err.println( + " SpannerExample addstoringindex my-instance example-db"); + System.err.println( + " SpannerExample readstoringindex my-instance example-db"); + System.err.println( + " SpannerExample readonlytransaction my-instance example-db"); + System.err.println( + " SpannerExample readstaledata my-instance example-db"); + System.err.println( + " SpannerExample addcommittimestamp my-instance example-db"); + System.err.println( + " SpannerExample updatewithtimestamp my-instance example-db"); + System.err.println( + " SpannerExample querywithtimestamp my-instance example-db"); + System.err.println( + " SpannerExample createtablewithtimestamp my-instance example-db"); + System.err.println( + " SpannerExample writewithtimestamp my-instance example-db"); + System.err.println( + " SpannerExample queryperformancestable my-instance example-db"); System.exit(1); } diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 8f6a2e7457d..ced1ecc2d7b 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -85,7 +85,7 @@ public void testSample() throws Exception { out = runSample("query"); assertThat(out).contains("1 1 Total Junk"); runSample("addmarketingbudget"); - + // wait for 15 seconds to elapse and then run an update, and query for stale data lastUpdateDataTimeInMillis = System.currentTimeMillis(); while (System.currentTimeMillis() < lastUpdateDataTimeInMillis + 16000) { @@ -116,6 +116,23 @@ public void testSample() throws Exception { out = runSample("readonlytransaction"); assertThat(out.replaceAll("[\r\n]+", " ")).containsMatch("(Total Junk.*){2}"); + + out = runSample("addcommittimestamp"); + assertThat(out).contains("Added LastUpdateTime as a commit timestamp column"); + + runSample("updatewithtimestamp"); + out = runSample("querywithtimestamp"); + assertThat(out).contains("1 1 1000000"); + assertThat(out).contains("2 2 750000"); + + out = runSample("createtablewithtimestamp"); + assertThat(out).contains("Created Performances table in database"); + + runSample("writewithtimestamp"); + out = runSample("queryperformancestable"); + assertThat(out).contains("1 4 2017-10-05 11000"); + assertThat(out).contains("1 19 2017-11-02 15000"); + assertThat(out).contains("2 42 2017-12-23 7000"); } private String formatForTest(String name) { From 88d7c763c5a83199d1ed804717beff94e0661ed4 Mon Sep 17 00:00:00 2001 From: Jisha Abubaker Date: Fri, 15 Jun 2018 09:31:32 -0700 Subject: [PATCH 17/79] samples: Adding Spanner STRUCT param samples (#1128) * Adding Spanner STRUCT param samples * updating tests * updating pom.xml * equals => isEqualTo * updating BOM to 0.52.0-alpha * test updates * test updates * fixing checkstyle errors --- .../com/example/spanner/SpannerSample.java | 390 +++++++++++++----- .../com/example/spanner/SpannerSampleIT.java | 14 + 2 files changed, 296 insertions(+), 108 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 51133ca4d62..015d245618a 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -17,6 +17,7 @@ package com.example.spanner; import static com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import static com.google.cloud.spanner.Type.StructField; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; @@ -34,6 +35,7 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.TransactionContext; +import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Value; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; @@ -43,21 +45,22 @@ import java.util.concurrent.TimeUnit; /** - * Example code for using the Cloud Spanner API. This example demonstrates all the common - * operations that can be done on Cloud Spanner. These are:

    + * Example code for using the Cloud Spanner API. This example demonstrates all the common operations + * that can be done on Cloud Spanner. These are: + * + *

    + * *

      - *
    • Creating a Cloud Spanner database. - *
    • Writing, reading and executing SQL queries. - *
    • Writing data using a read-write transaction. - *
    • Using an index to read and execute SQL queries over data. - *
    • Using commit timestamp for tracking when a record was last updated. + *
    • Creating a Cloud Spanner database. + *
    • Writing, reading and executing SQL queries. + *
    • Writing data using a read-write transaction. + *
    • Using an index to read and execute SQL queries over data. + *
    • Using commit timestamp for tracking when a record was last updated. *
    */ public class SpannerSample { - /** - * Class to contain singer sample data. - */ + /** Class to contain singer sample data. */ static class Singer { final long singerId; @@ -71,9 +74,7 @@ static class Singer { } } - /** - * Class to contain album sample data. - */ + /** Class to contain album sample data. */ static class Album { final long singerId; @@ -87,9 +88,7 @@ static class Album { } } - /** - * Class to contain performance sample data. - */ + /** Class to contain performance sample data. */ static class Performance { final long singerId; @@ -133,8 +132,8 @@ static class Performance { // [START spanner_create_database] static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { - Operation op = dbAdminClient - .createDatabase( + Operation op = + dbAdminClient.createDatabase( id.getInstanceId().getInstance(), id.getDatabase(), Arrays.asList( @@ -157,8 +156,8 @@ static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { // [START spanner_create_table_with_timestamp_column] static void createTableWithTimestamp(DatabaseAdminClient dbAdminClient, DatabaseId id) { - Operation op = dbAdminClient - .updateDatabaseDdl( + Operation op = + dbAdminClient.updateDatabaseDdl( id.getInstanceId().getInstance(), id.getDatabase(), Arrays.asList( @@ -169,7 +168,8 @@ static void createTableWithTimestamp(DatabaseAdminClient dbAdminClient, Database + " Revenue INT64, \n" + " LastUpdateTime TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)\n" + ") PRIMARY KEY (SingerId, VenueId, EventDate),\n" - + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), null); + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), + null); op.waitFor().getResult(); System.out.println("Created Performances table in database: [" + id + "]"); } @@ -245,7 +245,8 @@ static void read(DatabaseClient dbClient) { ResultSet resultSet = dbClient .singleUse() - .read("Albums", + .read( + "Albums", // KeySet.all() can be used to read all rows in a table. KeySet exposes other // methods to read only a subset of the table. KeySet.all(), @@ -259,10 +260,13 @@ static void read(DatabaseClient dbClient) { // [START spanner_add_column] static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"), - null).waitFor(); + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"), + null) + .waitFor(); System.out.println("Added MarketingBudget column"); } // [END spanner_add_column] @@ -367,10 +371,13 @@ static void queryMarketingBudget(DatabaseClient dbClient) { // [START spanner_create_index] static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), - null).waitFor(); + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), + null) + .waitFor(); System.out.println("Added AlbumsByAlbumTitle index"); } // [END spanner_create_index] @@ -379,17 +386,21 @@ static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { // "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)". // [START spanner_query_data_with_index] static void queryUsingIndex(DatabaseClient dbClient) { - Statement statement = Statement - // We use FORCE_INDEX hint to specify which index to use. For more details see - // https://cloud.google.com/spanner/docs/query-syntax#from-clause - .newBuilder("SELECT AlbumId, AlbumTitle, MarketingBudget\n" - + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}\n" - + "WHERE AlbumTitle >= @StartTitle AND AlbumTitle < @EndTitle") - // We use @BoundParameters to help speed up frequently executed queries. - // For more details see https://cloud.google.com/spanner/docs/sql-best-practices - .bind("StartTitle").to("Aardvark") - .bind("EndTitle").to("Goo") - .build(); + Statement statement = + Statement + // We use FORCE_INDEX hint to specify which index to use. For more details see + // https://cloud.google.com/spanner/docs/query-syntax#from-clause + .newBuilder( + "SELECT AlbumId, AlbumTitle, MarketingBudget\n" + + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}\n" + + "WHERE AlbumTitle >= @StartTitle AND AlbumTitle < @EndTitle") + // We use @BoundParameters to help speed up frequently executed queries. + // For more details see https://cloud.google.com/spanner/docs/sql-best-practices + .bind("StartTitle") + .to("Aardvark") + .bind("EndTitle") + .to("Goo") + .build(); ResultSet resultSet = dbClient.singleUse().executeQuery(statement); while (resultSet.next()) { @@ -420,11 +431,14 @@ static void readUsingIndex(DatabaseClient dbClient) { // [START spanner_create_storing_index] static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList( - "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)"), - null).waitFor(); + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)"), + null) + .waitFor(); System.out.println("Added AlbumsByAlbumTitle2 index"); } // [END spanner_create_storing_index] @@ -482,12 +496,12 @@ static void readStaleData(DatabaseClient dbClient) { ResultSet resultSet = dbClient .singleUse(TimestampBound.ofExactStaleness(15, TimeUnit.SECONDS)) - .read("Albums", - KeySet.all(), - Arrays.asList("SingerId", "AlbumId", "MarketingBudget")); + .read("Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "MarketingBudget")); while (resultSet.next()) { System.out.printf( - "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), + "%d %d %s\n", + resultSet.getLong(0), + resultSet.getLong(1), resultSet.isNull(2) ? "NULL" : resultSet.getLong("MarketingBudget")); } } @@ -495,12 +509,15 @@ static void readStaleData(DatabaseClient dbClient) { // [START spanner_add_timestamp_column] static void addCommitTimestamp(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient.updateDatabaseDdl(dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList( - "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " - + "OPTIONS (allow_commit_timestamp=true)"), - null).waitFor(); + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " + + "OPTIONS (allow_commit_timestamp=true)"), + null) + .waitFor(); System.out.println("Added LastUpdateTime as a commit timestamp column in Albums table."); } // [END spanner_add_timestamp_column] @@ -547,9 +564,10 @@ static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { ResultSet resultSet = dbClient .singleUse() - .executeQuery(Statement.of( - "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime FROM Albums" - + " ORDER BY LastUpdateTime DESC")); + .executeQuery( + Statement.of( + "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime FROM Albums" + + " ORDER BY LastUpdateTime DESC")); while (resultSet.next()) { System.out.printf( "%d %d %s %s\n", @@ -569,9 +587,10 @@ static void queryPerformancesTable(DatabaseClient dbClient) { ResultSet resultSet = dbClient .singleUse() - .executeQuery(Statement.of( - "SELECT SingerId, VenueId, EventDate, Revenue, LastUpdateTime FROM Performances" - + " ORDER BY LastUpdateTime DESC")); + .executeQuery( + Statement.of( + "SELECT SingerId, VenueId, EventDate, Revenue, LastUpdateTime FROM Performances" + + " ORDER BY LastUpdateTime DESC")); while (resultSet.next()) { System.out.printf( "%d %d %s %s %s\n", @@ -585,7 +604,162 @@ static void queryPerformancesTable(DatabaseClient dbClient) { } } - static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, String command, + // [START spanner_write_data_for_struct_queries] + static void writeStructExampleData(DatabaseClient dbClient) { + final List singers = + Arrays.asList( + new Singer(6, "Elena", "Campbell"), + new Singer(7, "Gabriel", "Wright"), + new Singer(8, "Benjamin", "Martinez"), + new Singer(9, "Hannah", "Harris")); + + List mutations = new ArrayList<>(); + for (Singer singer : singers) { + mutations.add( + Mutation.newInsertBuilder("Singers") + .set("SingerId") + .to(singer.singerId) + .set("FirstName") + .to(singer.firstName) + .set("LastName") + .to(singer.lastName) + .build()); + } + dbClient.write(mutations); + System.out.println("Inserted example data for struct parameter queries."); + } + // [END spanner_write_data_for_struct_queries] + + static void queryWithStruct(DatabaseClient dbClient) { + // [START spanner_create_struct_with_data] + Struct name = + Struct.newBuilder().set("FirstName").to("Elena").set("LastName").to("Campbell").build(); + // [END spanner_create_struct_with_data] + + // [START spanner_query_data_with_struct] + Statement s = + Statement.newBuilder( + "SELECT SingerId FROM Singers " + + "WHERE STRUCT(FirstName, LastName) " + + "= @name") + .bind("name") + .to(name) + .build(); + + ResultSet resultSet = dbClient.singleUse().executeQuery(s); + while (resultSet.next()) { + System.out.printf("%d\n", resultSet.getLong("SingerId")); + } + // [END spanner_query_data_with_struct] + } + + static void queryWithArrayOfStruct(DatabaseClient dbClient) { + // [START spanner_create_user_defined_struct] + Type nameType = + Type.struct( + Arrays.asList( + StructField.of("FirstName", Type.string()), + StructField.of("LastName", Type.string()))); + // [END spanner_create_user_defined_struct] + + // [START spanner_create_array_of_struct_with_data] + List bandMembers = new ArrayList<>(); + bandMembers.add( + Struct.newBuilder().set("FirstName").to("Elena").set("LastName").to("Campbell").build()); + bandMembers.add( + Struct.newBuilder().set("FirstName").to("Gabriel").set("LastName").to("Wright").build()); + bandMembers.add( + Struct.newBuilder().set("FirstName").to("Benjamin").set("LastName").to("Martinez").build()); + // [END spanner_create_array_of_struct_with_data] + + // [START spanner_query_data_with_array_of_struct] + Statement s = + Statement.newBuilder( + "SELECT SingerId FROM Singers WHERE " + + "STRUCT(FirstName, LastName) " + + "IN UNNEST(@names)") + .bind("names") + .toStructArray(nameType, bandMembers) + .build(); + + ResultSet resultSet = dbClient.singleUse().executeQuery(s); + while (resultSet.next()) { + System.out.printf("%d\n", resultSet.getLong("SingerId")); + } + // [END spanner_query_data_with_array_of_struct] + } + + // [START spanner_field_access_on_struct_parameters] + static void queryStructField(DatabaseClient dbClient) { + Statement s = + Statement.newBuilder("SELECT SingerId FROM Singers WHERE FirstName = @name.FirstName") + .bind("name") + .to( + Struct.newBuilder() + .set("FirstName") + .to("Elena") + .set("LastName") + .to("Campbell") + .build()) + .build(); + + ResultSet resultSet = dbClient.singleUse().executeQuery(s); + while (resultSet.next()) { + System.out.printf("%d\n", resultSet.getLong("SingerId")); + } + } + // [END spanner_field_access_on_struct_parameters] + + // [START spanner_field_access_on_nested_struct_parameters] + static void queryNestedStructField(DatabaseClient dbClient) { + Type nameType = + Type.struct( + Arrays.asList( + StructField.of("FirstName", Type.string()), + StructField.of("LastName", Type.string()))); + + Struct songInfo = + Struct.newBuilder() + .set("song_name") + .to("Imagination") + .set("artistNames") + .toStructArray( + nameType, + Arrays.asList( + Struct.newBuilder() + .set("FirstName") + .to("Elena") + .set("LastName") + .to("Campbell") + .build(), + Struct.newBuilder() + .set("FirstName") + .to("Hannah") + .set("LastName") + .to("Harris") + .build())) + .build(); + Statement s = + Statement.newBuilder( + "SELECT SingerId, @song_info.song_name " + + "FROM Singers WHERE " + + "STRUCT(FirstName, LastName) " + + "IN UNNEST(@song_info.artistNames)") + .bind("song_info") + .to(songInfo) + .build(); + + ResultSet resultSet = dbClient.singleUse().executeQuery(s); + while (resultSet.next()) { + System.out.printf("%d %s\n", resultSet.getLong("SingerId"), resultSet.getString(1)); + } + } + // [END spanner_field_access_on_nested_struct_parameters] + + static void run( + DatabaseClient dbClient, + DatabaseAdminClient dbAdminClient, + String command, DatabaseId database) { switch (command) { case "createdatabase": @@ -651,6 +825,21 @@ static void run(DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, Stri case "queryperformancestable": queryPerformancesTable(dbClient); break; + case "writestructdata": + writeStructExampleData(dbClient); + break; + case "querywithstruct": + queryWithStruct(dbClient); + break; + case "querywitharrayofstruct": + queryWithArrayOfStruct(dbClient); + break; + case "querystructfield": + queryStructField(dbClient); + break; + case "querynestedstructfield": + queryNestedStructField(dbClient); + break; default: printUsageAndExit(); } @@ -661,48 +850,32 @@ static void printUsageAndExit() { System.err.println(" SpannerExample "); System.err.println(""); System.err.println("Examples:"); - System.err.println( - " SpannerExample createdatabase my-instance example-db"); - System.err.println( - " SpannerExample write my-instance example-db"); - System.err.println( - " SpannerExample query my-instance example-db"); - System.err.println( - " SpannerExample read my-instance example-db"); - System.err.println( - " SpannerExample addmarketingbudget my-instance example-db"); - System.err.println( - " SpannerExample update my-instance example-db"); - System.err.println( - " SpannerExample writetransaction my-instance example-db"); - System.err.println( - " SpannerExample querymarketingbudget my-instance example-db"); - System.err.println( - " SpannerExample addindex my-instance example-db"); - System.err.println( - " SpannerExample readindex my-instance example-db"); - System.err.println( - " SpannerExample queryindex my-instance example-db"); - System.err.println( - " SpannerExample addstoringindex my-instance example-db"); - System.err.println( - " SpannerExample readstoringindex my-instance example-db"); - System.err.println( - " SpannerExample readonlytransaction my-instance example-db"); - System.err.println( - " SpannerExample readstaledata my-instance example-db"); - System.err.println( - " SpannerExample addcommittimestamp my-instance example-db"); - System.err.println( - " SpannerExample updatewithtimestamp my-instance example-db"); - System.err.println( - " SpannerExample querywithtimestamp my-instance example-db"); - System.err.println( - " SpannerExample createtablewithtimestamp my-instance example-db"); - System.err.println( - " SpannerExample writewithtimestamp my-instance example-db"); - System.err.println( - " SpannerExample queryperformancestable my-instance example-db"); + System.err.println(" SpannerExample createdatabase my-instance example-db"); + System.err.println(" SpannerExample write my-instance example-db"); + System.err.println(" SpannerExample query my-instance example-db"); + System.err.println(" SpannerExample read my-instance example-db"); + System.err.println(" SpannerExample addmarketingbudget my-instance example-db"); + System.err.println(" SpannerExample update my-instance example-db"); + System.err.println(" SpannerExample writetransaction my-instance example-db"); + System.err.println(" SpannerExample querymarketingbudget my-instance example-db"); + System.err.println(" SpannerExample addindex my-instance example-db"); + System.err.println(" SpannerExample readindex my-instance example-db"); + System.err.println(" SpannerExample queryindex my-instance example-db"); + System.err.println(" SpannerExample addstoringindex my-instance example-db"); + System.err.println(" SpannerExample readstoringindex my-instance example-db"); + System.err.println(" SpannerExample readonlytransaction my-instance example-db"); + System.err.println(" SpannerExample readstaledata my-instance example-db"); + System.err.println(" SpannerExample addcommittimestamp my-instance example-db"); + System.err.println(" SpannerExample updatewithtimestamp my-instance example-db"); + System.err.println(" SpannerExample querywithtimestamp my-instance example-db"); + System.err.println(" SpannerExample createtablewithtimestamp my-instance example-db"); + System.err.println(" SpannerExample writewithtimestamp my-instance example-db"); + System.err.println(" SpannerExample queryperformancestable my-instance example-db"); + System.err.println(" SpannerExample writestructdata my-instance example-db"); + System.err.println(" SpannerExample querywithstruct my-instance example-db"); + System.err.println(" SpannerExample querywitharrayofstruct my-instance example-db"); + System.err.println(" SpannerExample querystructfield my-instance example-db"); + System.err.println(" SpannerExample querynestedstructfield my-instance example-db"); System.exit(1); } @@ -720,9 +893,10 @@ public static void main(String[] args) throws Exception { // This will return the default project id based on the environment. String clientProject = spanner.getOptions().getProjectId(); if (!db.getInstanceId().getProject().equals(clientProject)) { - System.err.println("Invalid project specified. Project in the database id should match" - + "the project name set in the environment variable GCLOUD_PROJECT. Expected: " - + clientProject); + System.err.println( + "Invalid project specified. Project in the database id should match" + + "the project name set in the environment variable GCLOUD_PROJECT. Expected: " + + clientProject); printUsageAndExit(); } // [START init_client] diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index ced1ecc2d7b..cebcbeedbd6 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -133,6 +133,20 @@ public void testSample() throws Exception { assertThat(out).contains("1 4 2017-10-05 11000"); assertThat(out).contains("1 19 2017-11-02 15000"); assertThat(out).contains("2 42 2017-12-23 7000"); + + runSample("writestructdata"); + out = runSample("querywithstruct"); + assertThat(out).startsWith("6\n"); + + out = runSample("querywitharrayofstruct"); + assertThat(out).startsWith("6\n7"); + + out = runSample("querystructfield"); + assertThat(out).startsWith("6\n"); + + out = runSample("querynestedstructfield"); + assertThat(out).contains("6 Imagination\n"); + assertThat(out).contains("9 Imagination\n"); } private String formatForTest(String name) { From 475016c13810429f51f2b5f8077649e706d490da Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Tue, 9 Oct 2018 13:15:59 -0700 Subject: [PATCH 18/79] samples: Add Cloud Spanner DML/PDML samples. (#1227) --- .../com/example/spanner/SpannerSample.java | 291 +++++++++++++++++- .../com/example/spanner/SpannerSampleIT.java | 47 +++ 2 files changed, 334 insertions(+), 4 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 015d245618a..a849df70887 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -581,6 +581,22 @@ static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { } // [END spanner_query_data_with_timestamp_column] + static void querySingersTable(DatabaseClient dbClient) { + ResultSet resultSet = + dbClient + .singleUse() + .executeQuery( + Statement.of( + "SELECT SingerId, FirstName, LastName FROM Singers")); + while (resultSet.next()) { + System.out.printf( + "%s %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } + } + static void queryPerformancesTable(DatabaseClient dbClient) { // Rows without an explicit value for Revenue will have a Revenue equal to // null. @@ -640,8 +656,8 @@ static void queryWithStruct(DatabaseClient dbClient) { Statement s = Statement.newBuilder( "SELECT SingerId FROM Singers " - + "WHERE STRUCT(FirstName, LastName) " - + "= @name") + + "WHERE STRUCT(FirstName, LastName) " + + "= @name") .bind("name") .to(name) .build(); @@ -676,8 +692,8 @@ static void queryWithArrayOfStruct(DatabaseClient dbClient) { Statement s = Statement.newBuilder( "SELECT SingerId FROM Singers WHERE " - + "STRUCT(FirstName, LastName) " - + "IN UNNEST(@names)") + + "STRUCT(FirstName, LastName) " + + "IN UNNEST(@names)") .bind("names") .toStructArray(nameType, bandMembers) .build(); @@ -756,6 +772,229 @@ static void queryNestedStructField(DatabaseClient dbClient) { } // [END spanner_field_access_on_nested_struct_parameters] + // [START spanner_dml_standard_insert] + static void insertUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName) " + + " VALUES (10, 'Virginia', 'Watson')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record inserted.\n", rowCount); + return null; + } + }); + } + // [END spanner_dml_standard_insert] + + // [START spanner_dml_standard_update] + static void updateUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + String sql = + "UPDATE Albums " + + "SET MarketingBudget = MarketingBudget * 2 " + + "WHERE SingerId = 1 and AlbumId = 1"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record updated.\n", rowCount); + return null; + } + }); + } + // [END spanner_dml_standard_update] + + // [START spanner_dml_standard_delete] + static void deleteUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + String sql = "DELETE FROM Singers WHERE FirstName = 'Alice'"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record deleted.\n", rowCount); + return null; + } + }); + } + // [END spanner_dml_standard_delete] + + // [START spanner_dml_standard_update_with_timestamp] + static void updateUsingDmlWithTimestamp(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + String sql = + "UPDATE Albums " + + "SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP() WHERE SingerId = 1"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d records updated.\n", rowCount); + return null; + } + }); + } + // [END spanner_dml_standard_update_with_timestamp] + + // [START spanner_dml_write_then_read] + static void writeAndReadUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + // Insert record. + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName) " + + " VALUES (11, 'Timothy', 'Campbell')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record inserted.\n", rowCount); + // Read newly inserted record. + sql = "SELECT FirstName, LastName FROM Singers WHERE SingerId = 11"; + ResultSet resultSet = transaction.executeQuery(Statement.of(sql)); + while (resultSet.next()) { + System.out.printf( + "%s %s\n", resultSet.getString("FirstName"), resultSet.getString("LastName")); + } + return null; + } + }); + } + // [END spanner_dml_write_then_read] + + // [START spanner_dml_structs] + static void updateUsingDmlWithStruct(DatabaseClient dbClient) { + Struct name = + Struct.newBuilder().set("FirstName").to("Timothy").set("LastName").to("Campbell").build(); + Statement s = + Statement.newBuilder( + "UPDATE Singers SET LastName = 'Grant' " + + "WHERE STRUCT(FirstName, LastName) " + + "= @name") + .bind("name") + .to(name) + .build(); + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + long rowCount = transaction.executeUpdate(s); + System.out.printf("%d record updated.\n", rowCount); + return null; + } + }); + } + // [END spanner_dml_structs] + + // [START spanner_dml_getting_started_insert] + static void writeUsingDml(DatabaseClient dbClient) { + // Insert 4 singer records + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES " + + "(12, 'Melissa', 'Garcia'), " + + "(13, 'Russell', 'Morales'), " + + "(14, 'Jacqueline', 'Long'), " + + "(15, 'Dylan', 'Shaw')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d records inserted.\n", rowCount); + return null; + } + }); + } + // [END spanner_dml_getting_started_insert] + + // [START spanner_dml_getting_started_update] + static void writeWithTransactionUsingDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + // Transfer marketing budget from one album to another. We do it in a transaction to + // ensure that the transfer is atomic. + String sql1 = + "SELECT MarketingBudget from Albums WHERE SingerId = 1 and AlbumId = 1"; + ResultSet resultSet = transaction.executeQuery(Statement.of(sql1)); + long album1Budget = 0; + while (resultSet.next()) { + album1Budget = resultSet.getLong("MarketingBudget"); + } + // Transaction will only be committed if this condition still holds at the time of + // commit. Otherwise it will be aborted and the callable will be rerun by the + // client library. + if (album1Budget >= 300000) { + String sql2 = + "SELECT MarketingBudget from Albums WHERE SingerId = 2 and AlbumId = 2"; + ResultSet resultSet2 = transaction.executeQuery(Statement.of(sql2)); + long album2Budget = 0; + while (resultSet.next()) { + album2Budget = resultSet2.getLong("MarketingBudget"); + } + long transfer = 200000; + album2Budget += transfer; + album1Budget -= transfer; + Statement updateStatement = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 1 and AlbumId = 1") + .bind("AlbumBudget") + .to(album1Budget) + .build(); + transaction.executeUpdate(updateStatement); + Statement updateStatement2 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 2 and AlbumId = 2") + .bind("AlbumBudget") + .to(album2Budget) + .build(); + transaction.executeUpdate(updateStatement2); + } + return null; + } + }); + } + // [END spanner_dml_getting_started_update] + + // [START spanner_dml_partitioned_update] + static void updateUsingPartitionedDml(DatabaseClient dbClient) { + String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"; + long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql)); + System.out.printf("%d records updated.\n", rowCount); + } + // [END spanner_dml_partitioned_update] + + // [START spanner_dml_partitioned_delete] + static void deleteUsingPartitionedDml(DatabaseClient dbClient) { + String sql = "DELETE FROM Singers WHERE SingerId > 10"; + long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql)); + System.out.printf("%d records deleted.\n", rowCount); + } + // [END spanner_dml_partitioned_delete] + static void run( DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, @@ -822,6 +1061,9 @@ static void run( case "writewithtimestamp": writeExampleDataWithTimestamp(dbClient); break; + case "querysingerstable": + querySingersTable(dbClient); + break; case "queryperformancestable": queryPerformancesTable(dbClient); break; @@ -840,6 +1082,36 @@ static void run( case "querynestedstructfield": queryNestedStructField(dbClient); break; + case "insertusingdml": + insertUsingDml(dbClient); + break; + case "updateusingdml": + updateUsingDml(dbClient); + break; + case "deleteusingdml": + deleteUsingDml(dbClient); + break; + case "updateusingdmlwithtimestamp": + updateUsingDmlWithTimestamp(dbClient); + break; + case "writeandreadusingdml": + writeAndReadUsingDml(dbClient); + break; + case "updateusingdmlwithstruct": + updateUsingDmlWithStruct(dbClient); + break; + case "writeusingdml": + writeUsingDml(dbClient); + break; + case "writewithtransactionusingdml": + writeWithTransactionUsingDml(dbClient); + break; + case "updateusingpartitioneddml": + updateUsingPartitionedDml(dbClient); + break; + case "deleteusingpartitioneddml": + deleteUsingPartitionedDml(dbClient); + break; default: printUsageAndExit(); } @@ -870,12 +1142,23 @@ static void printUsageAndExit() { System.err.println(" SpannerExample querywithtimestamp my-instance example-db"); System.err.println(" SpannerExample createtablewithtimestamp my-instance example-db"); System.err.println(" SpannerExample writewithtimestamp my-instance example-db"); + System.err.println(" SpannerExample querysingerstable my-instance example-db"); System.err.println(" SpannerExample queryperformancestable my-instance example-db"); System.err.println(" SpannerExample writestructdata my-instance example-db"); System.err.println(" SpannerExample querywithstruct my-instance example-db"); System.err.println(" SpannerExample querywitharrayofstruct my-instance example-db"); System.err.println(" SpannerExample querystructfield my-instance example-db"); System.err.println(" SpannerExample querynestedstructfield my-instance example-db"); + System.err.println(" SpannerExample insertusingdml my-instance example-db"); + System.err.println(" SpannerExample updateusingdml my-instance example-db"); + System.err.println(" SpannerExample deleteusingdml my-instance example-db"); + System.err.println(" SpannerExample updateusingdmlwithtimestamp my-instance example-db"); + System.err.println(" SpannerExample writeandreadusingdml my-instance example-db"); + System.err.println(" SpannerExample updateusingdmlwithstruct my-instance example-db"); + System.err.println(" SpannerExample writeusingdml my-instance example-db"); + System.err.println(" SpannerExample writewithtransactionusingdml my-instance example-db"); + System.err.println(" SpannerExample updateusingpartitioneddml my-instance example-db"); + System.err.println(" SpannerExample deleteusingpartitioneddml my-instance example-db"); System.exit(1); } diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index cebcbeedbd6..2dee126ad6d 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -147,6 +147,53 @@ public void testSample() throws Exception { out = runSample("querynestedstructfield"); assertThat(out).contains("6 Imagination\n"); assertThat(out).contains("9 Imagination\n"); + + runSample("insertusingdml"); + out = runSample("querysingerstable"); + assertThat(out).contains("Virginia Watson"); + + runSample("updateusingdml"); + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 2000000"); + + runSample("deleteusingdml"); + out = runSample("querysingerstable"); + assertThat(out).doesNotContain("Alice Trentor"); + + out = runSample("updateusingdmlwithtimestamp"); + assertThat(out).contains("2 records updated"); + + out = runSample("writeandreadusingdml"); + assertThat(out).contains("Timothy Campbell"); + + runSample("updateusingdmlwithstruct"); + out = runSample("querysingerstable"); + assertThat(out).contains("Timothy Grant"); + + runSample("writeusingdml"); + out = runSample("querysingerstable"); + assertThat(out).contains("Melissa Garcia"); + assertThat(out).contains("Russell Morales"); + assertThat(out).contains("Jacqueline Long"); + assertThat(out).contains("Dylan Shaw"); + + runSample("writewithtransactionusingdml"); + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 1800000"); + assertThat(out).contains("2 2 200000"); + + runSample("updateusingpartitioneddml"); + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 1800000"); + assertThat(out).contains("2 2 100000"); + + runSample("deleteusingpartitioneddml"); + out = runSample("querysingerstable"); + assertThat(out).doesNotContain("Timothy Grant"); + assertThat(out).doesNotContain("Melissa Garcia"); + assertThat(out).doesNotContain("Russell Morales"); + assertThat(out).doesNotContain("Jacqueline Long"); + assertThat(out).doesNotContain("Dylan Shaw"); } private String formatForTest(String name) { From 2b95beff978a7d4704685a8df27eb7d010e0114c Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Tue, 13 Nov 2018 07:41:45 -0800 Subject: [PATCH 19/79] samples: Update Cloud Spanner sample to use latest client. (#1256) * Update Cloud Spanner sample to use latest client. * Update additional methods to use new client. * Address review comments. --- .../com/example/spanner/SpannerSample.java | 158 +++++++++++++----- 1 file changed, 114 insertions(+), 44 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index a849df70887..c686118422a 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -19,6 +19,7 @@ import static com.google.cloud.spanner.TransactionRunner.TransactionCallable; import static com.google.cloud.spanner.Type.StructField; +import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; @@ -26,10 +27,11 @@ import com.google.cloud.spanner.Key; import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Operation; import com.google.cloud.spanner.ReadOnlyTransaction; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Struct; @@ -42,6 +44,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** @@ -56,6 +59,8 @@ *
  • Writing data using a read-write transaction. *
  • Using an index to read and execute SQL queries over data. *
  • Using commit timestamp for tracking when a record was last updated. + *
  • Using Google API Extensions for Java to make thread-safe requests via + * long-running operations. http://googleapis.github.io/gax-java/ * */ public class SpannerSample { @@ -132,7 +137,7 @@ static class Performance { // [START spanner_create_database] static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { - Operation op = + OperationFuture op = dbAdminClient.createDatabase( id.getInstanceId().getInstance(), id.getDatabase(), @@ -149,14 +154,24 @@ static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { + " AlbumTitle STRING(MAX)\n" + ") PRIMARY KEY (SingerId, AlbumId),\n" + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE")); - Database db = op.waitFor().getResult(); - System.out.println("Created database [" + db.getId() + "]"); + try { + // Initiate the request which returns an OperationFuture. + Database db = op.get(); + System.out.println("Created database [" + db.getId() + "]"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } } // [END spanner_create_database] // [START spanner_create_table_with_timestamp_column] static void createTableWithTimestamp(DatabaseAdminClient dbAdminClient, DatabaseId id) { - Operation op = + OperationFuture op = dbAdminClient.updateDatabaseDdl( id.getInstanceId().getInstance(), id.getDatabase(), @@ -170,8 +185,18 @@ static void createTableWithTimestamp(DatabaseAdminClient dbAdminClient, Database + ") PRIMARY KEY (SingerId, VenueId, EventDate),\n" + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), null); - op.waitFor().getResult(); - System.out.println("Created Performances table in database: [" + id + "]"); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Created Performances table in database: [" + id + "]"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } } // [END spanner_create_table_with_timestamp_column] @@ -260,14 +285,25 @@ static void read(DatabaseClient dbClient) { // [START spanner_add_column] static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"), - null) - .waitFor(); - System.out.println("Added MarketingBudget column"); + OperationFuture op = + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added MarketingBudget column"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } } // [END spanner_add_column] @@ -371,14 +407,25 @@ static void queryMarketingBudget(DatabaseClient dbClient) { // [START spanner_create_index] static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), - null) - .waitFor(); - System.out.println("Added AlbumsByAlbumTitle index"); + OperationFuture op = + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added AlbumsByAlbumTitle index"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } } // [END spanner_create_index] @@ -431,15 +478,27 @@ static void readUsingIndex(DatabaseClient dbClient) { // [START spanner_create_storing_index] static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList( - "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)"), - null) - .waitFor(); - System.out.println("Added AlbumsByAlbumTitle2 index"); + OperationFuture op = + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) " + + "STORING (MarketingBudget)"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added AlbumsByAlbumTitle2 index"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } } // [END spanner_create_storing_index] @@ -509,16 +568,27 @@ static void readStaleData(DatabaseClient dbClient) { // [START spanner_add_timestamp_column] static void addCommitTimestamp(DatabaseAdminClient adminClient, DatabaseId dbId) { - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList( - "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " - + "OPTIONS (allow_commit_timestamp=true)"), - null) - .waitFor(); - System.out.println("Added LastUpdateTime as a commit timestamp column in Albums table."); + OperationFuture op = + adminClient + .updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " + + "OPTIONS (allow_commit_timestamp=true)"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Added LastUpdateTime as a commit timestamp column in Albums table."); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } } // [END spanner_add_timestamp_column] @@ -605,8 +675,8 @@ static void queryPerformancesTable(DatabaseClient dbClient) { .singleUse() .executeQuery( Statement.of( - "SELECT SingerId, VenueId, EventDate, Revenue, LastUpdateTime FROM Performances" - + " ORDER BY LastUpdateTime DESC")); + "SELECT SingerId, VenueId, EventDate, Revenue, LastUpdateTime " + + "FROM Performances ORDER BY LastUpdateTime DESC")); while (resultSet.next()) { System.out.printf( "%d %d %s %s %s\n", From 1393105504d2f2fe19f665c8f53f7a2c085123ef Mon Sep 17 00:00:00 2001 From: Robin Reynolds-Haertle Date: Mon, 26 Nov 2018 12:11:18 -0800 Subject: [PATCH 20/79] samples: Add code snippet to delete rows. (#1269) * Update SpannerSample.java * Update SpannerSampleIT.java --- .../com/example/spanner/SpannerSample.java | 23 +++++++++++++++++++ .../com/example/spanner/SpannerSampleIT.java | 5 ++++ 2 files changed, 28 insertions(+) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index c686118422a..0c2a62f68c2 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -251,6 +251,25 @@ static void writeExampleData(DatabaseClient dbClient) { } // [END spanner_insert_data] + // [START spanner_delete_data] + static void deleteExampleData(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + + // KeySet.all() can be used to delete all the rows in a table. + mutations.add(Mutation.delete("Albums", KeySet.all())); + + // KeySet.singleKey() can be used to delete one row at a time. + for (Singer singer : SINGERS) { + mutations.add( + Mutation.delete("Singers", + KeySet.singleKey(Key.newBuilder().append(singer.singerId).build()))); + } + + dbClient.write(mutations); + System.out.printf("Records deleted.\n"); + } + // [END spanner_delete_data] + // [START spanner_query_data] static void query(DatabaseClient dbClient) { // singleUse() can be used to execute a single read or query against Cloud Spanner. @@ -1077,6 +1096,9 @@ static void run( case "write": writeExampleData(dbClient); break; + case "delete": + deleteExampleData(dbClient); + break; case "query": query(dbClient); break; @@ -1194,6 +1216,7 @@ static void printUsageAndExit() { System.err.println("Examples:"); System.err.println(" SpannerExample createdatabase my-instance example-db"); System.err.println(" SpannerExample write my-instance example-db"); + System.err.println(" SpannerExample delete my-instance example-db"); System.err.println(" SpannerExample query my-instance example-db"); System.err.println(" SpannerExample read my-instance example-db"); System.err.println(" SpannerExample addmarketingbudget my-instance example-db"); diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 2dee126ad6d..54c9407f772 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -79,6 +79,11 @@ public void testSample() throws Exception { runSample("write"); + out = runSample("delete"); + assertThat(out).contains("Records deleted."); + + runSample("write"); + out = runSample("read"); assertThat(out).contains("1 1 Total Junk"); From d5d6b92e250c1b34d69174e22ae1b39d929542f0 Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Mon, 25 Mar 2019 15:30:10 -0700 Subject: [PATCH 21/79] samples: Add Cloud Spanner Batch DML sample (#1361) --- .../com/example/spanner/SpannerSample.java | 40 ++++++++++++++++++- .../com/example/spanner/SpannerSampleIT.java | 4 ++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 0c2a62f68c2..5bfe9e082b3 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -30,6 +30,7 @@ import com.google.cloud.spanner.ReadOnlyTransaction; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerBatchUpdateException; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.SpannerOptions; @@ -1084,6 +1085,39 @@ static void deleteUsingPartitionedDml(DatabaseClient dbClient) { } // [END spanner_dml_partitioned_delete] + // [START spanner_dml_batch_update] + static void updateUsingBatchDml(DatabaseClient dbClient) { + dbClient + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + List stmts = new ArrayList(); + String sql = "INSERT INTO Albums " + + "(SingerId, AlbumId, AlbumTitle, MarketingBudget) " + + "VALUES (1, 3, 'Test Album Title', 10000) "; + stmts.add(Statement.of(sql)); + sql = "UPDATE Albums " + + "SET MarketingBudget = MarketingBudget * 2 " + + "WHERE SingerId = 1 and AlbumId = 3"; + stmts.add(Statement.of(sql)); + long [] rowCounts; + try { + rowCounts = transaction.batchUpdate(stmts); + } catch (SpannerBatchUpdateException e) { + rowCounts = e.getUpdateCounts(); + } + for (int i = 0; i < rowCounts.length; i++) { + System.out.printf( + "%d record updated by stmt %d.\n", rowCounts[i], i); + } + return null; + } + }); + } + // [END spanner_dml_batch_update] + static void run( DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, @@ -1203,7 +1237,10 @@ static void run( break; case "deleteusingpartitioneddml": deleteUsingPartitionedDml(dbClient); - break; + break; + case "updateusingbatchdml": + updateUsingBatchDml(dbClient); + break; default: printUsageAndExit(); } @@ -1252,6 +1289,7 @@ static void printUsageAndExit() { System.err.println(" SpannerExample writewithtransactionusingdml my-instance example-db"); System.err.println(" SpannerExample updateusingpartitioneddml my-instance example-db"); System.err.println(" SpannerExample deleteusingpartitioneddml my-instance example-db"); + System.err.println(" SpannerExample updateusingbatchdml my-instance example-db"); System.exit(1); } diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 54c9407f772..b20262fc9d9 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -199,6 +199,10 @@ public void testSample() throws Exception { assertThat(out).doesNotContain("Russell Morales"); assertThat(out).doesNotContain("Jacqueline Long"); assertThat(out).doesNotContain("Dylan Shaw"); + + out = runSample("updateusingbatchdml"); + assertThat(out).contains("1 record updated by stmt 0"); + assertThat(out).contains("1 record updated by stmt 1"); } private String formatForTest(String name) { From 3c9de844b6eb9be453e7c5a866c241c59d62dacf Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Tue, 14 May 2019 17:14:40 -0700 Subject: [PATCH 22/79] samples: Add queryWithParameter to Cloud Spanner sample. (#1417) --- .../com/example/spanner/SpannerSample.java | 27 +++++++++++++++++++ .../com/example/spanner/SpannerSampleIT.java | 4 ++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 5bfe9e082b3..86f7b02adfa 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -1013,6 +1013,29 @@ public Void run(TransactionContext transaction) throws Exception { } // [END spanner_dml_getting_started_insert] + // [START spanner_query_with_parameter] + static void queryWithParameter(DatabaseClient dbClient) { + Statement statement = + Statement + .newBuilder( + "SELECT SingerId, FirstName, LastName\n" + + "FROM Singers\n" + + "WHERE LastName = @lastName") + .bind("lastName") + .to("Garcia") + .build(); + + ResultSet resultSet = dbClient.singleUse().executeQuery(statement); + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } + } + // [END spanner_query_with_parameter] + // [START spanner_dml_getting_started_update] static void writeWithTransactionUsingDml(DatabaseClient dbClient) { dbClient @@ -1229,6 +1252,9 @@ static void run( case "writeusingdml": writeUsingDml(dbClient); break; + case "querywithparameter": + queryWithParameter(dbClient); + break; case "writewithtransactionusingdml": writeWithTransactionUsingDml(dbClient); break; @@ -1286,6 +1312,7 @@ static void printUsageAndExit() { System.err.println(" SpannerExample writeandreadusingdml my-instance example-db"); System.err.println(" SpannerExample updateusingdmlwithstruct my-instance example-db"); System.err.println(" SpannerExample writeusingdml my-instance example-db"); + System.err.println(" SpannerExample queryWithParameter my-instance example-db"); System.err.println(" SpannerExample writewithtransactionusingdml my-instance example-db"); System.err.println(" SpannerExample updateusingpartitioneddml my-instance example-db"); System.err.println(" SpannerExample deleteusingpartitioneddml my-instance example-db"); diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index b20262fc9d9..6c30a13d6c8 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -144,7 +144,7 @@ public void testSample() throws Exception { assertThat(out).startsWith("6\n"); out = runSample("querywitharrayofstruct"); - assertThat(out).startsWith("6\n7"); + assertThat(out).startsWith("8\n7\n6"); out = runSample("querystructfield"); assertThat(out).startsWith("6\n"); @@ -181,6 +181,8 @@ public void testSample() throws Exception { assertThat(out).contains("Russell Morales"); assertThat(out).contains("Jacqueline Long"); assertThat(out).contains("Dylan Shaw"); + out = runSample("querywithparameter"); + assertThat(out).contains("12 Melissa Garcia"); runSample("writewithtransactionusingdml"); out = runSample("querymarketingbudget"); From 85139a2658e0bcaf863de1b39f47c27d241306a0 Mon Sep 17 00:00:00 2001 From: Jeff Williams Date: Tue, 28 May 2019 12:04:37 -0700 Subject: [PATCH 23/79] samples: Cloud Spanner: Fix samples that transfer money between albums (#1438) * Cloud Spanner: Fix samples that transfer money between albums The `writeWithTransactionUsingDml` method moved $200,000 from Album1 to Album2. However, Album1 is initialized with a budget of $100,000. As a result, you couldn't work through the getting started doc [1] from start to finish. Also, there were some unnecessary inconsistencies between `writeWithTransaction` and `writeWithTransactionUsingDml`, and neither method explicitly compared the transfer amount with the source album's existing balance. I changed the methods so they both transfer $200,000 from Album2 to Album1. I also added explicit checks against the existing balance, and I updated the tests as needed. (This is an updated version of #1432.) [1]: https://cloud.google.com/spanner/docs/getting-started/java/ * get the budget from the correct result set; update tests --- .../com/example/spanner/SpannerSample.java | 126 +++++++++--------- .../com/example/spanner/SpannerSampleIT.java | 17 +-- 2 files changed, 67 insertions(+), 76 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 86f7b02adfa..980ef61c723 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -60,8 +60,8 @@ *
  • Writing data using a read-write transaction. *
  • Using an index to read and execute SQL queries over data. *
  • Using commit timestamp for tracking when a record was last updated. - *
  • Using Google API Extensions for Java to make thread-safe requests via - * long-running operations. http://googleapis.github.io/gax-java/ + *
  • Using Google API Extensions for Java to make thread-safe requests via long-running + * operations. http://googleapis.github.io/gax-java/ * */ public class SpannerSample { @@ -262,13 +262,13 @@ static void deleteExampleData(DatabaseClient dbClient) { // KeySet.singleKey() can be used to delete one row at a time. for (Singer singer : SINGERS) { mutations.add( - Mutation.delete("Singers", - KeySet.singleKey(Key.newBuilder().append(singer.singerId).build()))); + Mutation.delete( + "Singers", KeySet.singleKey(Key.newBuilder().append(singer.singerId).build()))); } dbClient.write(mutations); System.out.printf("Records deleted.\n"); - } + } // [END spanner_delete_data] // [START spanner_query_data] @@ -306,12 +306,11 @@ static void read(DatabaseClient dbClient) { // [START spanner_add_column] static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseId dbId) { OperationFuture op = - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"), - null); + adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"), + null); try { // Initiate the request which returns an OperationFuture. op.get(); @@ -372,12 +371,12 @@ public Void run(TransactionContext transaction) throws Exception { // Transaction will only be committed if this condition still holds at the time of // commit. Otherwise it will be aborted and the callable will be rerun by the // client library. - if (album2Budget >= 300000) { + long transfer = 200000; + if (album2Budget >= transfer) { long album1Budget = transaction .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget")) .getLong(0); - long transfer = 200000; album1Budget += transfer; album2Budget -= transfer; transaction.buffer( @@ -428,12 +427,11 @@ static void queryMarketingBudget(DatabaseClient dbClient) { // [START spanner_create_index] static void addIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { OperationFuture op = - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), - null); + adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)"), + null); try { // Initiate the request which returns an OperationFuture. op.get(); @@ -499,14 +497,13 @@ static void readUsingIndex(DatabaseClient dbClient) { // [START spanner_create_storing_index] static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { OperationFuture op = - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList( - "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) " - + "STORING (MarketingBudget)"), - null); + adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) " + + "STORING (MarketingBudget)"), + null); try { // Initiate the request which returns an OperationFuture. op.get(); @@ -589,14 +586,13 @@ static void readStaleData(DatabaseClient dbClient) { // [START spanner_add_timestamp_column] static void addCommitTimestamp(DatabaseAdminClient adminClient, DatabaseId dbId) { OperationFuture op = - adminClient - .updateDatabaseDdl( - dbId.getInstanceId().getInstance(), - dbId.getDatabase(), - Arrays.asList( - "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " - + "OPTIONS (allow_commit_timestamp=true)"), - null); + adminClient.updateDatabaseDdl( + dbId.getInstanceId().getInstance(), + dbId.getDatabase(), + Arrays.asList( + "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP " + + "OPTIONS (allow_commit_timestamp=true)"), + null); try { // Initiate the request which returns an OperationFuture. op.get(); @@ -675,9 +671,7 @@ static void querySingersTable(DatabaseClient dbClient) { ResultSet resultSet = dbClient .singleUse() - .executeQuery( - Statement.of( - "SELECT SingerId, FirstName, LastName FROM Singers")); + .executeQuery(Statement.of("SELECT SingerId, FirstName, LastName FROM Singers")); while (resultSet.next()) { System.out.printf( "%s %s %s\n", @@ -1016,8 +1010,7 @@ public Void run(TransactionContext transaction) throws Exception { // [START spanner_query_with_parameter] static void queryWithParameter(DatabaseClient dbClient) { Statement statement = - Statement - .newBuilder( + Statement.newBuilder( "SELECT SingerId, FirstName, LastName\n" + "FROM Singers\n" + "WHERE LastName = @lastName") @@ -1047,40 +1040,40 @@ public Void run(TransactionContext transaction) throws Exception { // Transfer marketing budget from one album to another. We do it in a transaction to // ensure that the transfer is atomic. String sql1 = - "SELECT MarketingBudget from Albums WHERE SingerId = 1 and AlbumId = 1"; + "SELECT MarketingBudget from Albums WHERE SingerId = 2 and AlbumId = 2"; ResultSet resultSet = transaction.executeQuery(Statement.of(sql1)); - long album1Budget = 0; + long album2Budget = 0; while (resultSet.next()) { - album1Budget = resultSet.getLong("MarketingBudget"); + album2Budget = resultSet.getLong("MarketingBudget"); } // Transaction will only be committed if this condition still holds at the time of // commit. Otherwise it will be aborted and the callable will be rerun by the // client library. - if (album1Budget >= 300000) { + long transfer = 200000; + if (album2Budget >= transfer) { String sql2 = - "SELECT MarketingBudget from Albums WHERE SingerId = 2 and AlbumId = 2"; + "SELECT MarketingBudget from Albums WHERE SingerId = 1 and AlbumId = 1"; ResultSet resultSet2 = transaction.executeQuery(Statement.of(sql2)); - long album2Budget = 0; - while (resultSet.next()) { - album2Budget = resultSet2.getLong("MarketingBudget"); + long album1Budget = 0; + while (resultSet2.next()) { + album1Budget = resultSet2.getLong("MarketingBudget"); } - long transfer = 200000; - album2Budget += transfer; - album1Budget -= transfer; + album1Budget += transfer; + album2Budget -= transfer; Statement updateStatement = Statement.newBuilder( - "UPDATE Albums " - + "SET MarketingBudget = @AlbumBudget " - + "WHERE SingerId = 1 and AlbumId = 1") + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 1 and AlbumId = 1") .bind("AlbumBudget") .to(album1Budget) .build(); transaction.executeUpdate(updateStatement); Statement updateStatement2 = Statement.newBuilder( - "UPDATE Albums " - + "SET MarketingBudget = @AlbumBudget " - + "WHERE SingerId = 2 and AlbumId = 2") + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 2 and AlbumId = 2") .bind("AlbumBudget") .to(album2Budget) .build(); @@ -1108,7 +1101,7 @@ static void deleteUsingPartitionedDml(DatabaseClient dbClient) { } // [END spanner_dml_partitioned_delete] - // [START spanner_dml_batch_update] + // [START spanner_dml_batch_update] static void updateUsingBatchDml(DatabaseClient dbClient) { dbClient .readWriteTransaction() @@ -1117,23 +1110,24 @@ static void updateUsingBatchDml(DatabaseClient dbClient) { @Override public Void run(TransactionContext transaction) throws Exception { List stmts = new ArrayList(); - String sql = "INSERT INTO Albums " - + "(SingerId, AlbumId, AlbumTitle, MarketingBudget) " - + "VALUES (1, 3, 'Test Album Title', 10000) "; + String sql = + "INSERT INTO Albums " + + "(SingerId, AlbumId, AlbumTitle, MarketingBudget) " + + "VALUES (1, 3, 'Test Album Title', 10000) "; stmts.add(Statement.of(sql)); - sql = "UPDATE Albums " + sql = + "UPDATE Albums " + "SET MarketingBudget = MarketingBudget * 2 " + "WHERE SingerId = 1 and AlbumId = 3"; stmts.add(Statement.of(sql)); - long [] rowCounts; + long[] rowCounts; try { rowCounts = transaction.batchUpdate(stmts); } catch (SpannerBatchUpdateException e) { rowCounts = e.getUpdateCounts(); } for (int i = 0; i < rowCounts.length; i++) { - System.out.printf( - "%d record updated by stmt %d.\n", rowCounts[i], i); + System.out.printf("%d record updated by stmt %d.\n", rowCounts[i], i); } return null; } @@ -1298,7 +1292,7 @@ static void printUsageAndExit() { System.err.println(" SpannerExample querywithtimestamp my-instance example-db"); System.err.println(" SpannerExample createtablewithtimestamp my-instance example-db"); System.err.println(" SpannerExample writewithtimestamp my-instance example-db"); - System.err.println(" SpannerExample querysingerstable my-instance example-db"); + System.err.println(" SpannerExample querysingerstable my-instance example-db"); System.err.println(" SpannerExample queryperformancestable my-instance example-db"); System.err.println(" SpannerExample writestructdata my-instance example-db"); System.err.println(" SpannerExample querywithstruct my-instance example-db"); diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 6c30a13d6c8..680eb3cdc92 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -22,7 +22,6 @@ import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; - import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.UUID; @@ -32,9 +31,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** - * Unit tests for {@code SpannerSample} - */ +/** Unit tests for {@code SpannerSample} */ @RunWith(JUnit4.class) @SuppressWarnings("checkstyle:abbreviationaswordinname") public class SpannerSampleIT { @@ -50,7 +47,7 @@ private String runSample(String command) throws Exception { ByteArrayOutputStream bout = new ByteArrayOutputStream(); PrintStream out = new PrintStream(bout); System.setOut(out); - SpannerSample.main(new String[]{command, instanceId, databaseId}); + SpannerSample.main(new String[] {command, instanceId, databaseId}); System.setOut(stdOut); return bout.toString(); } @@ -185,13 +182,13 @@ public void testSample() throws Exception { assertThat(out).contains("12 Melissa Garcia"); runSample("writewithtransactionusingdml"); - out = runSample("querymarketingbudget"); - assertThat(out).contains("1 1 1800000"); - assertThat(out).contains("2 2 200000"); - + out = runSample("querymarketingbudget"); + assertThat(out).contains("1 1 2200000"); + assertThat(out).contains("2 2 550000"); + runSample("updateusingpartitioneddml"); out = runSample("querymarketingbudget"); - assertThat(out).contains("1 1 1800000"); + assertThat(out).contains("1 1 2200000"); assertThat(out).contains("2 2 100000"); runSample("deleteusingpartitioneddml"); From a0200687762c8df89b031abe859b20b071ddd503 Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Thu, 6 Jun 2019 11:11:08 -0700 Subject: [PATCH 24/79] samples: Update Spanner sample to show close of resources. (#1447) * Update Spanner sample to show close of resources. * Update close statements to be try-with-resources. * Lint. * Address review comments. --- .../com/example/spanner/SpannerSample.java | 283 ++++++++++-------- 1 file changed, 153 insertions(+), 130 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 980ef61c723..7458973e7cd 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -273,32 +273,31 @@ static void deleteExampleData(DatabaseClient dbClient) { // [START spanner_query_data] static void query(DatabaseClient dbClient) { - // singleUse() can be used to execute a single read or query against Cloud Spanner. - ResultSet resultSet = - dbClient - .singleUse() - .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums")); - while (resultSet.next()) { - System.out.printf( - "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient + .singleUse() // Execute a single read or query against Cloud Spanner. + .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } } } // [END spanner_query_data] // [START spanner_read_data] static void read(DatabaseClient dbClient) { - ResultSet resultSet = - dbClient + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient .singleUse() .read( "Albums", - // KeySet.all() can be used to read all rows in a table. KeySet exposes other - // methods to read only a subset of the table. - KeySet.all(), - Arrays.asList("SingerId", "AlbumId", "AlbumTitle")); - while (resultSet.next()) { - System.out.printf( - "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + KeySet.all(), // Read all rows in a table. + Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } } } // [END spanner_read_data] @@ -407,19 +406,20 @@ public Void run(TransactionContext transaction) throws Exception { // [START spanner_query_data_with_new_column] static void queryMarketingBudget(DatabaseClient dbClient) { // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to - // null. - ResultSet resultSet = - dbClient + // null. A try-with-resource block is used to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = dbClient .singleUse() - .executeQuery(Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums")); - while (resultSet.next()) { - System.out.printf( - "%d %d %s\n", - resultSet.getLong("SingerId"), - resultSet.getLong("AlbumId"), - // We check that the value is non null. ResultSet getters can only be used to retrieve - // non null values. - resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + .executeQuery(Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + } } } // [END spanner_query_data_with_new_column] @@ -466,30 +466,32 @@ static void queryUsingIndex(DatabaseClient dbClient) { .bind("EndTitle") .to("Goo") .build(); - - ResultSet resultSet = dbClient.singleUse().executeQuery(statement); - while (resultSet.next()) { - System.out.printf( - "%d %s %s\n", - resultSet.getLong("AlbumId"), - resultSet.getString("AlbumTitle"), - resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("AlbumId"), + resultSet.getString("AlbumTitle"), + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + } } } // [END spanner_query_data_with_index] // [START spanner_read_data_with_index] static void readUsingIndex(DatabaseClient dbClient) { - ResultSet resultSet = - dbClient + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient .singleUse() .readUsingIndex( "Albums", "AlbumsByAlbumTitle", KeySet.all(), - Arrays.asList("AlbumId", "AlbumTitle")); - while (resultSet.next()) { - System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1)); + Arrays.asList("AlbumId", "AlbumTitle"))) { + while (resultSet.next()) { + System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1)); + } } } // [END spanner_read_data_with_index] @@ -524,20 +526,21 @@ static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { // [START spanner_read_data_with_storing_index] static void readStoringIndex(DatabaseClient dbClient) { // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget. - ResultSet resultSet = - dbClient + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient .singleUse() .readUsingIndex( "Albums", "AlbumsByAlbumTitle2", KeySet.all(), - Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget")); - while (resultSet.next()) { - System.out.printf( - "%d %s %s\n", - resultSet.getLong(0), - resultSet.getString(1), - resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget"))) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong(0), + resultSet.getString(1), + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget")); + } } } // [END spanner_read_data_with_storing_index] @@ -555,13 +558,15 @@ static void readOnlyTransaction(DatabaseClient dbClient) { "%d %d %s\n", queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2)); } - ResultSet readResultSet = + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet readResultSet = transaction.read( - "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle")); - while (readResultSet.next()) { - System.out.printf( - "%d %d %s\n", - readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2)); + "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) { + while (readResultSet.next()) { + System.out.printf( + "%d %d %s\n", + readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2)); + } } } } @@ -569,16 +574,18 @@ static void readOnlyTransaction(DatabaseClient dbClient) { // [START spanner_read_stale_data] static void readStaleData(DatabaseClient dbClient) { - ResultSet resultSet = - dbClient + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient .singleUse(TimestampBound.ofExactStaleness(15, TimeUnit.SECONDS)) - .read("Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "MarketingBudget")); - while (resultSet.next()) { - System.out.printf( - "%d %d %s\n", - resultSet.getLong(0), - resultSet.getLong(1), - resultSet.isNull(2) ? "NULL" : resultSet.getLong("MarketingBudget")); + .read( + "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "MarketingBudget"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong(0), + resultSet.getLong(1), + resultSet.isNull(2) ? "NULL" : resultSet.getLong("MarketingBudget")); + } } } // [END spanner_read_stale_data] @@ -646,61 +653,64 @@ static void updateWithTimestamp(DatabaseClient dbClient) { // [START spanner_query_data_with_timestamp_column] static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to - // null. - ResultSet resultSet = - dbClient + // null. A try-with-resource block is used to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = dbClient .singleUse() .executeQuery( Statement.of( "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime FROM Albums" - + " ORDER BY LastUpdateTime DESC")); - while (resultSet.next()) { - System.out.printf( - "%d %d %s %s\n", - resultSet.getLong("SingerId"), - resultSet.getLong("AlbumId"), - // We check that the value is non null. ResultSet getters can only be used to retrieve - // non null values. - resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"), - resultSet.isNull("LastUpdateTime") ? "NULL" : resultSet.getTimestamp("LastUpdateTime")); + + " ORDER BY LastUpdateTime DESC"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"), + resultSet.isNull("LastUpdateTime") ? "NULL" : resultSet.getTimestamp("LastUpdateTime")); + } } } // [END spanner_query_data_with_timestamp_column] static void querySingersTable(DatabaseClient dbClient) { - ResultSet resultSet = - dbClient + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient .singleUse() - .executeQuery(Statement.of("SELECT SingerId, FirstName, LastName FROM Singers")); - while (resultSet.next()) { - System.out.printf( - "%s %s %s\n", - resultSet.getLong("SingerId"), - resultSet.getString("FirstName"), - resultSet.getString("LastName")); + .executeQuery(Statement.of("SELECT SingerId, FirstName, LastName FROM Singers"))) { + while (resultSet.next()) { + System.out.printf( + "%s %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } } } static void queryPerformancesTable(DatabaseClient dbClient) { // Rows without an explicit value for Revenue will have a Revenue equal to - // null. - ResultSet resultSet = - dbClient + // null. A try-with-resource block is used to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = dbClient .singleUse() .executeQuery( Statement.of( "SELECT SingerId, VenueId, EventDate, Revenue, LastUpdateTime " - + "FROM Performances ORDER BY LastUpdateTime DESC")); - while (resultSet.next()) { - System.out.printf( - "%d %d %s %s %s\n", - resultSet.getLong("SingerId"), - resultSet.getLong("VenueId"), - resultSet.getDate("EventDate"), - // We check that the value is non null. ResultSet getters can only be used to retrieve - // non null values. - resultSet.isNull("Revenue") ? "NULL" : resultSet.getLong("Revenue"), - resultSet.getTimestamp("LastUpdateTime")); + + "FROM Performances ORDER BY LastUpdateTime DESC"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("VenueId"), + resultSet.getDate("EventDate"), + // We check that the value is non null. ResultSet getters can only be used to retrieve + // non null values. + resultSet.isNull("Revenue") ? "NULL" : resultSet.getLong("Revenue"), + resultSet.getTimestamp("LastUpdateTime")); + } } } @@ -745,10 +755,11 @@ static void queryWithStruct(DatabaseClient dbClient) { .bind("name") .to(name) .build(); - - ResultSet resultSet = dbClient.singleUse().executeQuery(s); - while (resultSet.next()) { - System.out.printf("%d\n", resultSet.getLong("SingerId")); + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { + while (resultSet.next()) { + System.out.printf("%d\n", resultSet.getLong("SingerId")); + } } // [END spanner_query_data_with_struct] } @@ -781,10 +792,11 @@ static void queryWithArrayOfStruct(DatabaseClient dbClient) { .bind("names") .toStructArray(nameType, bandMembers) .build(); - - ResultSet resultSet = dbClient.singleUse().executeQuery(s); - while (resultSet.next()) { - System.out.printf("%d\n", resultSet.getLong("SingerId")); + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { + while (resultSet.next()) { + System.out.printf("%d\n", resultSet.getLong("SingerId")); + } } // [END spanner_query_data_with_array_of_struct] } @@ -802,10 +814,11 @@ static void queryStructField(DatabaseClient dbClient) { .to("Campbell") .build()) .build(); - - ResultSet resultSet = dbClient.singleUse().executeQuery(s); - while (resultSet.next()) { - System.out.printf("%d\n", resultSet.getLong("SingerId")); + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { + while (resultSet.next()) { + System.out.printf("%d\n", resultSet.getLong("SingerId")); + } } } // [END spanner_field_access_on_struct_parameters] @@ -848,10 +861,11 @@ static void queryNestedStructField(DatabaseClient dbClient) { .bind("song_info") .to(songInfo) .build(); - - ResultSet resultSet = dbClient.singleUse().executeQuery(s); - while (resultSet.next()) { - System.out.printf("%d %s\n", resultSet.getLong("SingerId"), resultSet.getString(1)); + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { + while (resultSet.next()) { + System.out.printf("%d %s\n", resultSet.getLong("SingerId"), resultSet.getString(1)); + } } } // [END spanner_field_access_on_nested_struct_parameters] @@ -947,10 +961,15 @@ public Void run(TransactionContext transaction) throws Exception { System.out.printf("%d record inserted.\n", rowCount); // Read newly inserted record. sql = "SELECT FirstName, LastName FROM Singers WHERE SingerId = 11"; - ResultSet resultSet = transaction.executeQuery(Statement.of(sql)); - while (resultSet.next()) { - System.out.printf( - "%s %s\n", resultSet.getString("FirstName"), resultSet.getString("LastName")); + // We use a try-with-resource block to automatically release resources held by + // ResultSet. + try (ResultSet resultSet = transaction.executeQuery(Statement.of(sql))) { + while (resultSet.next()) { + System.out.printf( + "%s %s\n", + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } } return null; } @@ -1017,14 +1036,15 @@ static void queryWithParameter(DatabaseClient dbClient) { .bind("lastName") .to("Garcia") .build(); - - ResultSet resultSet = dbClient.singleUse().executeQuery(statement); - while (resultSet.next()) { - System.out.printf( - "%d %s %s\n", - resultSet.getLong("SingerId"), - resultSet.getString("FirstName"), - resultSet.getString("LastName")); + // We use a try-with-resource block to automatically release resources held by ResultSet. + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } } } // [END spanner_query_with_parameter] @@ -1337,11 +1357,14 @@ public static void main(String[] args) throws Exception { // [START init_client] DatabaseClient dbClient = spanner.getDatabaseClient(db); DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + // Use client here... // [END init_client] run(dbClient, dbAdminClient, command, db); + // [START init_client] } finally { spanner.close(); } + // [END init_client] System.out.println("Closed client"); } } From de256a752ce3c0b79b5a5de4384170bca0726b3f Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Fri, 7 Jun 2019 13:33:53 -0700 Subject: [PATCH 25/79] samples: Update Spanner usage command text. (#1449) --- .../src/main/java/com/example/spanner/SpannerSample.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 7458973e7cd..1b007becc22 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -1326,7 +1326,7 @@ static void printUsageAndExit() { System.err.println(" SpannerExample writeandreadusingdml my-instance example-db"); System.err.println(" SpannerExample updateusingdmlwithstruct my-instance example-db"); System.err.println(" SpannerExample writeusingdml my-instance example-db"); - System.err.println(" SpannerExample queryWithParameter my-instance example-db"); + System.err.println(" SpannerExample querywithparameter my-instance example-db"); System.err.println(" SpannerExample writewithtransactionusingdml my-instance example-db"); System.err.println(" SpannerExample updateusingpartitioneddml my-instance example-db"); System.err.println(" SpannerExample deleteusingpartitioneddml my-instance example-db"); From bbc2df47de832199b982b2891d828a2c15d044ee Mon Sep 17 00:00:00 2001 From: Jonathan Simon Date: Wed, 10 Jul 2019 15:27:12 -0700 Subject: [PATCH 26/79] samples: Add Datatypes examples to Spanner sample. (#1498) * Add Datatypes examples to Spanner sample. * Lint. * Lint. * Address review comments. --- .../com/example/spanner/SpannerSample.java | 385 ++++++++++++++++-- .../com/example/spanner/SpannerSampleIT.java | 34 ++ 2 files changed, 385 insertions(+), 34 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 1b007becc22..116ff0f1b1c 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -20,6 +20,8 @@ import static com.google.cloud.spanner.Type.StructField; import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.ByteArray; +import com.google.cloud.Date; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; @@ -40,8 +42,11 @@ import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Value; +import com.google.common.io.BaseEncoding; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import java.sql.Timestamp; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -110,6 +115,31 @@ static class Performance { } } + /** Class to contain venue sample data. */ + static class Venue { + + final long venueId; + final String venueName; + final String venueInfo; + final long capacity; + final Value availableDates; + final String lastContactDate; + final boolean outdoorVenue; + final float popularityScore; + + Venue(long venueId, String venueName, String venueInfo, long capacity, Value availableDates, + String lastContactDate, boolean outdoorVenue, float popularityScore) { + this.venueId = venueId; + this.venueName = venueName; + this.venueInfo = venueInfo; + this.capacity = capacity; + this.availableDates = availableDates; + this.lastContactDate = lastContactDate; + this.outdoorVenue = outdoorVenue; + this.popularityScore = popularityScore; + } + } + // [START spanner_insert_data] static final List SINGERS = Arrays.asList( @@ -136,6 +166,31 @@ static class Performance { new Performance(2, 42, "2017-12-23", 7000)); // [END spanner_insert_data_with_timestamp_column] + // [START spanner_insert_datatypes_data] + static Value availableDates1 = Value.dateArray(Arrays.asList( + Date.parseDate("2020-12-01"), + Date.parseDate("2020-12-02"), + Date.parseDate("2020-12-03"))); + static Value availableDates2 = Value.dateArray(Arrays.asList( + Date.parseDate("2020-11-01"), + Date.parseDate("2020-11-05"), + Date.parseDate("2020-11-15"))); + static Value availableDates3 = Value.dateArray(Arrays.asList( + Date.parseDate("2020-10-01"), + Date.parseDate("2020-10-07"))); + static String exampleBytes1 = BaseEncoding.base64().encode("Hello World 1".getBytes()); + static String exampleBytes2 = BaseEncoding.base64().encode("Hello World 2".getBytes()); + static String exampleBytes3 = BaseEncoding.base64().encode("Hello World 3".getBytes()); + static final List VENUES = + Arrays.asList( + new Venue(4, "Venue 4", exampleBytes1, 1800, + availableDates1, "2018-09-02", false, 0.85543f), + new Venue(19, "Venue 19", exampleBytes2, 6300, + availableDates2, "2019-01-15", true, 0.98716f), + new Venue(42, "Venue 42", exampleBytes3, 3000, + availableDates3, "2018-10-01", false, 0.72598f)); + // [END spanner_insert_datatypes_data] + // [START spanner_create_database] static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { OperationFuture op = @@ -143,17 +198,17 @@ static void createDatabase(DatabaseAdminClient dbAdminClient, DatabaseId id) { id.getInstanceId().getInstance(), id.getDatabase(), Arrays.asList( - "CREATE TABLE Singers (\n" - + " SingerId INT64 NOT NULL,\n" - + " FirstName STRING(1024),\n" - + " LastName STRING(1024),\n" - + " SingerInfo BYTES(MAX)\n" + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)" + ") PRIMARY KEY (SingerId)", - "CREATE TABLE Albums (\n" - + " SingerId INT64 NOT NULL,\n" - + " AlbumId INT64 NOT NULL,\n" - + " AlbumTitle STRING(MAX)\n" - + ") PRIMARY KEY (SingerId, AlbumId),\n" + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)" + + ") PRIMARY KEY (SingerId, AlbumId)," + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE")); try { // Initiate the request which returns an OperationFuture. @@ -177,13 +232,13 @@ static void createTableWithTimestamp(DatabaseAdminClient dbAdminClient, Database id.getInstanceId().getInstance(), id.getDatabase(), Arrays.asList( - "CREATE TABLE Performances (\n" - + " SingerId INT64 NOT NULL,\n" - + " VenueId INT64 NOT NULL,\n" - + " EventDate Date,\n" - + " Revenue INT64, \n" - + " LastUpdateTime TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)\n" - + ") PRIMARY KEY (SingerId, VenueId, EventDate),\n" + "CREATE TABLE Performances (" + + " SingerId INT64 NOT NULL," + + " VenueId INT64 NOT NULL," + + " EventDate Date," + + " Revenue INT64, " + + " LastUpdateTime TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)" + + ") PRIMARY KEY (SingerId, VenueId, EventDate)," + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"), null); try { @@ -273,7 +328,6 @@ static void deleteExampleData(DatabaseClient dbClient) { // [START spanner_query_data] static void query(DatabaseClient dbClient) { - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient .singleUse() // Execute a single read or query against Cloud Spanner. .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { @@ -287,7 +341,6 @@ static void query(DatabaseClient dbClient) { // [START spanner_read_data] static void read(DatabaseClient dbClient) { - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient .singleUse() .read( @@ -456,8 +509,8 @@ static void queryUsingIndex(DatabaseClient dbClient) { // We use FORCE_INDEX hint to specify which index to use. For more details see // https://cloud.google.com/spanner/docs/query-syntax#from-clause .newBuilder( - "SELECT AlbumId, AlbumTitle, MarketingBudget\n" - + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}\n" + "SELECT AlbumId, AlbumTitle, MarketingBudget " + + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} " + "WHERE AlbumTitle >= @StartTitle AND AlbumTitle < @EndTitle") // We use @BoundParameters to help speed up frequently executed queries. // For more details see https://cloud.google.com/spanner/docs/sql-best-practices @@ -466,7 +519,6 @@ static void queryUsingIndex(DatabaseClient dbClient) { .bind("EndTitle") .to("Goo") .build(); - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { while (resultSet.next()) { System.out.printf( @@ -481,7 +533,6 @@ static void queryUsingIndex(DatabaseClient dbClient) { // [START spanner_read_data_with_index] static void readUsingIndex(DatabaseClient dbClient) { - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient .singleUse() .readUsingIndex( @@ -526,7 +577,6 @@ static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { // [START spanner_read_data_with_storing_index] static void readStoringIndex(DatabaseClient dbClient) { // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget. - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient .singleUse() .readUsingIndex( @@ -558,7 +608,6 @@ static void readOnlyTransaction(DatabaseClient dbClient) { "%d %d %s\n", queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2)); } - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet readResultSet = transaction.read( "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) { @@ -574,7 +623,6 @@ static void readOnlyTransaction(DatabaseClient dbClient) { // [START spanner_read_stale_data] static void readStaleData(DatabaseClient dbClient) { - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient .singleUse(TimestampBound.ofExactStaleness(15, TimeUnit.SECONDS)) .read( @@ -676,7 +724,6 @@ static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { // [END spanner_query_data_with_timestamp_column] static void querySingersTable(DatabaseClient dbClient) { - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient .singleUse() .executeQuery(Statement.of("SELECT SingerId, FirstName, LastName FROM Singers"))) { @@ -755,7 +802,6 @@ static void queryWithStruct(DatabaseClient dbClient) { .bind("name") .to(name) .build(); - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { while (resultSet.next()) { System.out.printf("%d\n", resultSet.getLong("SingerId")); @@ -792,7 +838,6 @@ static void queryWithArrayOfStruct(DatabaseClient dbClient) { .bind("names") .toStructArray(nameType, bandMembers) .build(); - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { while (resultSet.next()) { System.out.printf("%d\n", resultSet.getLong("SingerId")); @@ -814,7 +859,6 @@ static void queryStructField(DatabaseClient dbClient) { .to("Campbell") .build()) .build(); - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { while (resultSet.next()) { System.out.printf("%d\n", resultSet.getLong("SingerId")); @@ -861,7 +905,6 @@ static void queryNestedStructField(DatabaseClient dbClient) { .bind("song_info") .to(songInfo) .build(); - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient.singleUse().executeQuery(s)) { while (resultSet.next()) { System.out.printf("%d %s\n", resultSet.getLong("SingerId"), resultSet.getString(1)); @@ -1030,13 +1073,12 @@ public Void run(TransactionContext transaction) throws Exception { static void queryWithParameter(DatabaseClient dbClient) { Statement statement = Statement.newBuilder( - "SELECT SingerId, FirstName, LastName\n" - + "FROM Singers\n" + "SELECT SingerId, FirstName, LastName " + + "FROM Singers " + "WHERE LastName = @lastName") .bind("lastName") .to("Garcia") .build(); - // We use a try-with-resource block to automatically release resources held by ResultSet. try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { while (resultSet.next()) { System.out.printf( @@ -1155,6 +1197,241 @@ public Void run(TransactionContext transaction) throws Exception { } // [END spanner_dml_batch_update] + // [START spanner_create_table_with_datatypes] + static void createTableWithDatatypes(DatabaseAdminClient dbAdminClient, DatabaseId id) { + OperationFuture op = + dbAdminClient.updateDatabaseDdl( + id.getInstanceId().getInstance(), + id.getDatabase(), + Arrays.asList( + "CREATE TABLE Venues (" + + " VenueId INT64 NOT NULL," + + " VenueName STRING(100)," + + " VenueInfo BYTES(MAX)," + + " Capacity INT64," + + " AvailableDates ARRAY," + + " LastContactDate DATE," + + " OutdoorVenue BOOL, " + + " PopularityScore FLOAT64, " + + " LastUpdateTime TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)" + + ") PRIMARY KEY (VenueId)"), + null); + try { + // Initiate the request which returns an OperationFuture. + op.get(); + System.out.println("Created Venues table in database: [" + id + "]"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_create_table_with_datatypes] + + // [START spanner_insert_datatypes_data] + static void writeDatatypesData(DatabaseClient dbClient) { + List mutations = new ArrayList<>(); + for (Venue venue : VENUES) { + mutations.add( + Mutation.newInsertBuilder("Venues") + .set("VenueId").to(venue.venueId) + .set("VenueName").to(venue.venueName) + .set("VenueInfo").to(venue.venueInfo) + .set("Capacity").to(venue.capacity) + .set("AvailableDates").to(venue.availableDates) + .set("LastContactDate").to(venue.lastContactDate) + .set("OutdoorVenue").to(venue.outdoorVenue) + .set("PopularityScore").to(venue.popularityScore) + .set("LastUpdateTime").to(Value.COMMIT_TIMESTAMP) + .build()); + } + dbClient.write(mutations); + } + // [END spanner_insert_datatypes_data] + + // [START spanner_query_with_array_parameter] + static void queryWithArray(DatabaseClient dbClient) { + Value exampleArray = Value.dateArray(Arrays.asList( + Date.parseDate("2020-10-01"), + Date.parseDate("2020-11-01"))); + + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName, AvailableDate FROM Venues v, " + + "UNNEST(v.AvailableDates) as AvailableDate " + + "WHERE AvailableDate in UNNEST(@availableDates)") + .bind("availableDates") + .to(exampleArray) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getDate("AvailableDate")); + } + } + } + // [END spanner_query_with_array_parameter] + + // [START spanner_query_with_bool_parameter] + static void queryWithBool(DatabaseClient dbClient) { + boolean exampleBool = true; + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName, OutdoorVenue FROM Venues " + + "WHERE OutdoorVenue = @outdoorVenue") + .bind("outdoorVenue") + .to(exampleBool) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %b\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getBoolean("OutdoorVenue")); + } + } + } + // [END spanner_query_with_bool_parameter] + + // [START spanner_query_with_bytes_parameter] + static void queryWithBytes(DatabaseClient dbClient) { + ByteArray exampleBytes = ByteArray.fromBase64( + BaseEncoding.base64().encode("Hello World 1".getBytes())); + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName FROM Venues " + + "WHERE VenueInfo = @venueInfo") + .bind("venueInfo") + .to(exampleBytes) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName")); + } + } + } + // [END spanner_query_with_bytes_parameter] + + // [START spanner_query_with_date_parameter] + static void queryWithDate(DatabaseClient dbClient) { + String exampleDate = "2019-01-01"; + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName, LastContactDate FROM Venues " + + "WHERE LastContactDate < @lastContactDate") + .bind("lastContactDate") + .to(exampleDate) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getDate("LastContactDate")); + } + } + } + // [END spanner_query_with_date_parameter] + + // [START spanner_query_with_float_parameter] + static void queryWithFloat(DatabaseClient dbClient) { + float exampleFloat = 0.8f; + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName, PopularityScore FROM Venues " + + "WHERE PopularityScore > @popularityScore") + .bind("popularityScore") + .to(exampleFloat) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %f\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getDouble("PopularityScore")); + } + } + } + // [END spanner_query_with_float_parameter] + + // [START spanner_query_with_int_parameter] + static void queryWithInt(DatabaseClient dbClient) { + long exampleInt = 3000; + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName, Capacity FROM Venues " + + "WHERE Capacity >= @capacity") + .bind("capacity") + .to(exampleInt) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %d\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getLong("Capacity")); + } + } + } + // [END spanner_query_with_int_parameter] + + // [START spanner_query_with_string_parameter] + static void queryWithString(DatabaseClient dbClient) { + String exampleString = "Venue 42"; + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName FROM Venues " + + "WHERE VenueName = @venueName") + .bind("venueName") + .to(exampleString) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName")); + } + } + } + // [END spanner_query_with_string_parameter] + + // [START spanner_query_with_timestamp_parameter] + static void queryWithTimestampParameter(DatabaseClient dbClient) { + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + Instant exampleTimestamp = timestamp.toInstant(); + Statement statement = + Statement.newBuilder( + "SELECT VenueId, VenueName, LastUpdateTime FROM Venues " + + "WHERE LastUpdateTime < @lastUpdateTime") + .bind("lastUpdateTime") + .to(exampleTimestamp.toString()) + .build(); + try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("VenueId"), + resultSet.getString("VenueName"), + resultSet.getTimestamp("LastUpdateTime")); + } + } + } + // [END spanner_query_with_timestamp_parameter] + static void run( DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, @@ -1281,6 +1558,36 @@ static void run( case "updateusingbatchdml": updateUsingBatchDml(dbClient); break; + case "createtablewithdatatypes": + createTableWithDatatypes(dbAdminClient, database); + break; + case "writedatatypesdata": + writeDatatypesData(dbClient); + break; + case "querywitharray": + queryWithArray(dbClient); + break; + case "querywithbool": + queryWithBool(dbClient); + break; + case "querywithbytes": + queryWithBytes(dbClient); + break; + case "querywithdate": + queryWithDate(dbClient); + break; + case "querywithfloat": + queryWithFloat(dbClient); + break; + case "querywithint": + queryWithInt(dbClient); + break; + case "querywithstring": + queryWithString(dbClient); + break; + case "querywithtimestampparameter": + queryWithTimestampParameter(dbClient); + break; default: printUsageAndExit(); } @@ -1331,6 +1638,16 @@ static void printUsageAndExit() { System.err.println(" SpannerExample updateusingpartitioneddml my-instance example-db"); System.err.println(" SpannerExample deleteusingpartitioneddml my-instance example-db"); System.err.println(" SpannerExample updateusingbatchdml my-instance example-db"); + System.err.println(" SpannerExample createtablewithdatatypes my-instance example-db"); + System.err.println(" SpannerExample writedatatypesdata my-instance example-db"); + System.err.println(" SpannerExample querywitharray my-instance example-db"); + System.err.println(" SpannerExample querywithbool my-instance example-db"); + System.err.println(" SpannerExample querywithbytes my-instance example-db"); + System.err.println(" SpannerExample querywithdate my-instance example-db"); + System.err.println(" SpannerExample querywithfloat my-instance example-db"); + System.err.println(" SpannerExample querywithint my-instance example-db"); + System.err.println(" SpannerExample querywithstring my-instance example-db"); + System.err.println(" SpannerExample querywithtimestampparameter my-instance example-db"); System.exit(1); } diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 680eb3cdc92..b04caffd515 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -202,6 +202,40 @@ public void testSample() throws Exception { out = runSample("updateusingbatchdml"); assertThat(out).contains("1 record updated by stmt 0"); assertThat(out).contains("1 record updated by stmt 1"); + + out = runSample("createtablewithdatatypes"); + assertThat(out).contains("Created Venues table in database"); + + runSample("writedatatypesdata"); + out = runSample("querywitharray"); + assertThat(out).contains("19 Venue 19 2020-11-01"); + assertThat(out).contains("42 Venue 42 2020-10-01"); + + out = runSample("querywithbool"); + assertThat(out).contains("19 Venue 19 true"); + + out = runSample("querywithbytes"); + assertThat(out).contains("4 Venue 4"); + + out = runSample("querywithdate"); + assertThat(out).contains("4 Venue 4 2018-09-02"); + assertThat(out).contains("42 Venue 42 2018-10-01"); + + out = runSample("querywithfloat"); + assertThat(out).contains("4 Venue 4 0.8"); + assertThat(out).contains("19 Venue 19 0.9"); + + out = runSample("querywithint"); + assertThat(out).contains("19 Venue 19 6300"); + assertThat(out).contains("42 Venue 42 3000"); + + out = runSample("querywithstring"); + assertThat(out).contains("42 Venue 42"); + + out = runSample("querywithtimestampparameter"); + assertThat(out).contains("4 Venue 4"); + assertThat(out).contains("19 Venue 19"); + assertThat(out).contains("42 Venue 42"); } private String formatForTest(String name) { From a3a96dc8e99a26f9136cbd8035c51b659273c22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Sat, 28 Dec 2019 01:28:59 +0100 Subject: [PATCH 27/79] samples: fix: Add ORDER BY to guarantee output order (#1903) --- .../com/example/spanner/SpannerSample.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 116ff0f1b1c..d2207bb3320 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -168,11 +168,11 @@ static class Venue { // [START spanner_insert_datatypes_data] static Value availableDates1 = Value.dateArray(Arrays.asList( - Date.parseDate("2020-12-01"), + Date.parseDate("2020-12-01"), Date.parseDate("2020-12-02"), Date.parseDate("2020-12-03"))); static Value availableDates2 = Value.dateArray(Arrays.asList( - Date.parseDate("2020-11-01"), + Date.parseDate("2020-11-01"), Date.parseDate("2020-11-05"), Date.parseDate("2020-11-15"))); static Value availableDates3 = Value.dateArray(Arrays.asList( @@ -185,7 +185,7 @@ static class Venue { Arrays.asList( new Venue(4, "Venue 4", exampleBytes1, 1800, availableDates1, "2018-09-02", false, 0.85543f), - new Venue(19, "Venue 19", exampleBytes2, 6300, + new Venue(19, "Venue 19", exampleBytes2, 6300, availableDates2, "2019-01-15", true, 0.98716f), new Venue(42, "Venue 42", exampleBytes3, 3000, availableDates3, "2018-10-01", false, 0.72598f)); @@ -328,7 +328,7 @@ static void deleteExampleData(DatabaseClient dbClient) { // [START spanner_query_data] static void query(DatabaseClient dbClient) { - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = dbClient .singleUse() // Execute a single read or query against Cloud Spanner. .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { while (resultSet.next()) { @@ -459,7 +459,7 @@ public Void run(TransactionContext transaction) throws Exception { // [START spanner_query_data_with_new_column] static void queryMarketingBudget(DatabaseClient dbClient) { // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to - // null. A try-with-resource block is used to automatically release resources held by + // null. A try-with-resource block is used to automatically release resources held by // ResultSet. try (ResultSet resultSet = dbClient .singleUse() @@ -834,7 +834,8 @@ static void queryWithArrayOfStruct(DatabaseClient dbClient) { Statement.newBuilder( "SELECT SingerId FROM Singers WHERE " + "STRUCT(FirstName, LastName) " - + "IN UNNEST(@names)") + + "IN UNNEST(@names) " + + "ORDER BY SingerId DESC") .bind("names") .toStructArray(nameType, bandMembers) .build(); @@ -1009,8 +1010,8 @@ public Void run(TransactionContext transaction) throws Exception { try (ResultSet resultSet = transaction.executeQuery(Statement.of(sql))) { while (resultSet.next()) { System.out.printf( - "%s %s\n", - resultSet.getString("FirstName"), + "%s %s\n", + resultSet.getString("FirstName"), resultSet.getString("LastName")); } } @@ -1255,7 +1256,7 @@ static void writeDatatypesData(DatabaseClient dbClient) { // [START spanner_query_with_array_parameter] static void queryWithArray(DatabaseClient dbClient) { Value exampleArray = Value.dateArray(Arrays.asList( - Date.parseDate("2020-10-01"), + Date.parseDate("2020-10-01"), Date.parseDate("2020-11-01"))); Statement statement = @@ -1319,7 +1320,7 @@ static void queryWithBytes(DatabaseClient dbClient) { resultSet.getString("VenueName")); } } - } + } // [END spanner_query_with_bytes_parameter] // [START spanner_query_with_date_parameter] From 6365c494ab77beeef6bf245178bbb043f1ecc998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Sat, 15 Feb 2020 02:16:26 +0100 Subject: [PATCH 28/79] samples: fix: default to PickFirstLoadBalancerProvider (#2063) The services/META-INF/io.grpc.LoadBalancerProvider file included by default in the jar with dependencies did not correspond with the default pick_first policy of AutoConfiguredLoadBalancerFactory. Fixes #2061 Co-authored-by: Les Vogel --- .../resources/META-INF/services/io.grpc.LoadBalancerProvider | 1 + 1 file changed, 1 insertion(+) create mode 100644 samples/snippets/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider diff --git a/samples/snippets/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider b/samples/snippets/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider new file mode 100644 index 00000000000..bbc367f8fc5 --- /dev/null +++ b/samples/snippets/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider @@ -0,0 +1 @@ +io.grpc.internal.PickFirstLoadBalancerProvider From 9dff625ab3815b2ef681bc98aac97e295e1a9c01 Mon Sep 17 00:00:00 2001 From: Dane Zeke Liergaard Date: Thu, 19 Mar 2020 15:09:57 -0700 Subject: [PATCH 29/79] samples: Correct error message for unset GOOGLE_CLOUD_PROJECT. (#2448) --- .../src/main/java/com/example/spanner/SpannerSample.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index d2207bb3320..754e62ff2bc 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -1667,8 +1667,8 @@ public static void main(String[] args) throws Exception { String clientProject = spanner.getOptions().getProjectId(); if (!db.getInstanceId().getProject().equals(clientProject)) { System.err.println( - "Invalid project specified. Project in the database id should match" - + "the project name set in the environment variable GCLOUD_PROJECT. Expected: " + "Invalid project specified. Project in the database id should match the" + + "project name set in the environment variable GOOGLE_CLOUD_PROJECT. Expected: " + clientProject); printUsageAndExit(); } From 917cd84ef5ee0097c73347a48f7426080b72e791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 25 Mar 2020 01:16:11 +0100 Subject: [PATCH 30/79] samples: feat: add QueryOptions to Spanner samples (#2323) --- .../com/example/spanner/SpannerSample.java | 48 +++++++++++++++++++ .../com/example/spanner/SpannerSampleIT.java | 7 ++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 754e62ff2bc..2db5d649a25 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -45,6 +45,7 @@ import com.google.common.io.BaseEncoding; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; @@ -1433,6 +1434,45 @@ static void queryWithTimestampParameter(DatabaseClient dbClient) { } // [END spanner_query_with_timestamp_parameter] + // [START spanner_create_client_with_query_options] + static void clientWithQueryOptions(DatabaseId db) { + SpannerOptions options = + SpannerOptions.newBuilder() + .setDefaultQueryOptions( + db, QueryOptions.newBuilder().setOptimizerVersion("1").build()) + .build(); + Spanner spanner = options.getService(); + DatabaseClient dbClient = spanner.getDatabaseClient(db); + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } + } + } + // [END spanner_create_client_with_query_options] + + // [START spanner_query_with_query_options] + static void queryWithQueryOptions(DatabaseClient dbClient) { + try (ResultSet resultSet = + dbClient + .singleUse() + .executeQuery( + Statement + .newBuilder("SELECT SingerId, AlbumId, AlbumTitle FROM Albums") + .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("1").build()) + .build())) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + } + } + } + // [END spanner_query_with_query_options] + static void run( DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, @@ -1589,6 +1629,12 @@ static void run( case "querywithtimestampparameter": queryWithTimestampParameter(dbClient); break; + case "clientwithqueryoptions": + clientWithQueryOptions(database); + break; + case "querywithqueryoptions": + queryWithQueryOptions(dbClient); + break; default: printUsageAndExit(); } @@ -1649,6 +1695,8 @@ static void printUsageAndExit() { System.err.println(" SpannerExample querywithint my-instance example-db"); System.err.println(" SpannerExample querywithstring my-instance example-db"); System.err.println(" SpannerExample querywithtimestampparameter my-instance example-db"); + System.err.println(" SpannerExample clientwithqueryoptions my-instance example-db"); + System.err.println(" SpannerExample querywithqueryoptions my-instance example-db"); System.exit(1); } diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index b04caffd515..0bf458ee201 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -216,7 +216,7 @@ public void testSample() throws Exception { out = runSample("querywithbytes"); assertThat(out).contains("4 Venue 4"); - + out = runSample("querywithdate"); assertThat(out).contains("4 Venue 4 2018-09-02"); assertThat(out).contains("42 Venue 42 2018-10-01"); @@ -236,6 +236,11 @@ public void testSample() throws Exception { assertThat(out).contains("4 Venue 4"); assertThat(out).contains("19 Venue 19"); assertThat(out).contains("42 Venue 42"); + + out = runSample("clientwithqueryoptions"); + assertThat(out).contains("1 1 Total Junk"); + out = runSample("querywithqueryoptions"); + assertThat(out).contains("1 1 Total Junk"); } private String formatForTest(String name) { From 5c3b3d7b5f14aff1f21392437fa43f807f754a00 Mon Sep 17 00:00:00 2001 From: Averi Kitsch Date: Fri, 27 Mar 2020 12:00:25 -0700 Subject: [PATCH 31/79] samples: update shared config (#2443) * update shared config * Update to 1.0.13 * lint * Fix linting * lint * fix imports Co-authored-by: Les Vogel --- .../java/com/example/spanner/BatchSample.java | 38 ++--- .../com/example/spanner/QuickstartSample.java | 4 +- .../com/example/spanner/SpannerSample.java | 135 ++++++++++-------- .../com/example/spanner/TracingSample.java | 29 ++-- 4 files changed, 111 insertions(+), 95 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/BatchSample.java b/samples/snippets/src/main/java/com/example/spanner/BatchSample.java index 1f44be61ae0..725f46d607f 100644 --- a/samples/snippets/src/main/java/com/example/spanner/BatchSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/BatchSample.java @@ -26,16 +26,13 @@ import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.TimestampBound; - import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -/** - * Sample showing how to run a query using the Batch API. - */ +/** Sample showing how to run a query using the Batch API. */ public class BatchSample { /** @@ -72,30 +69,33 @@ public static void main(String[] args) throws InterruptedException { AtomicInteger totalRecords = new AtomicInteger(0); try { - BatchClient batchClient = spanner.getBatchClient( - DatabaseId.of(options.getProjectId(), instanceId, databaseId)); + BatchClient batchClient = + spanner.getBatchClient(DatabaseId.of(options.getProjectId(), instanceId, databaseId)); final BatchReadOnlyTransaction txn = batchClient.batchReadOnlyTransaction(TimestampBound.strong()); // A Partition object is serializable and can be used from a different process. - List partitions = txn.partitionQuery(PartitionOptions.getDefaultInstance(), - Statement.of("SELECT SingerId, FirstName, LastName FROM Singers")); + List partitions = + txn.partitionQuery( + PartitionOptions.getDefaultInstance(), + Statement.of("SELECT SingerId, FirstName, LastName FROM Singers")); totalPartitions = partitions.size(); for (final Partition p : partitions) { - executor.execute(() -> { - try (ResultSet results = txn.execute(p)) { - while (results.next()) { - long singerId = results.getLong(0); - String firstName = results.getString(1); - String lastName = results.getString(2); - System.out.println("[" + singerId + "] " + firstName + " " + lastName); - totalRecords.getAndIncrement(); - } - } - }); + executor.execute( + () -> { + try (ResultSet results = txn.execute(p)) { + while (results.next()) { + long singerId = results.getLong(0); + String firstName = results.getString(1); + String lastName = results.getString(2); + System.out.println("[" + singerId + "] " + firstName + " " + lastName); + totalRecords.getAndIncrement(); + } + } + }); } } finally { executor.shutdown(); diff --git a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java index eaa64ed62aa..14aad267dd9 100644 --- a/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java @@ -45,8 +45,8 @@ public static void main(String... args) throws Exception { String databaseId = args[1]; try { // Creates a database client - DatabaseClient dbClient = spanner.getDatabaseClient(DatabaseId.of( - options.getProjectId(), instanceId, databaseId)); + DatabaseClient dbClient = + spanner.getDatabaseClient(DatabaseId.of(options.getProjectId(), instanceId, databaseId)); // Queries the database ResultSet resultSet = dbClient.singleUse().executeQuery(Statement.of("SELECT 1")); diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 2db5d649a25..ffffeeac68b 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -128,8 +128,15 @@ static class Venue { final boolean outdoorVenue; final float popularityScore; - Venue(long venueId, String venueName, String venueInfo, long capacity, Value availableDates, - String lastContactDate, boolean outdoorVenue, float popularityScore) { + Venue( + long venueId, + String venueName, + String venueInfo, + long capacity, + Value availableDates, + String lastContactDate, + boolean outdoorVenue, + float popularityScore) { this.venueId = venueId; this.venueName = venueName; this.venueInfo = venueInfo; @@ -168,28 +175,31 @@ static class Venue { // [END spanner_insert_data_with_timestamp_column] // [START spanner_insert_datatypes_data] - static Value availableDates1 = Value.dateArray(Arrays.asList( - Date.parseDate("2020-12-01"), - Date.parseDate("2020-12-02"), - Date.parseDate("2020-12-03"))); - static Value availableDates2 = Value.dateArray(Arrays.asList( - Date.parseDate("2020-11-01"), - Date.parseDate("2020-11-05"), - Date.parseDate("2020-11-15"))); - static Value availableDates3 = Value.dateArray(Arrays.asList( - Date.parseDate("2020-10-01"), - Date.parseDate("2020-10-07"))); + static Value availableDates1 = + Value.dateArray( + Arrays.asList( + Date.parseDate("2020-12-01"), + Date.parseDate("2020-12-02"), + Date.parseDate("2020-12-03"))); + static Value availableDates2 = + Value.dateArray( + Arrays.asList( + Date.parseDate("2020-11-01"), + Date.parseDate("2020-11-05"), + Date.parseDate("2020-11-15"))); + static Value availableDates3 = + Value.dateArray(Arrays.asList(Date.parseDate("2020-10-01"), Date.parseDate("2020-10-07"))); static String exampleBytes1 = BaseEncoding.base64().encode("Hello World 1".getBytes()); static String exampleBytes2 = BaseEncoding.base64().encode("Hello World 2".getBytes()); static String exampleBytes3 = BaseEncoding.base64().encode("Hello World 3".getBytes()); static final List VENUES = Arrays.asList( - new Venue(4, "Venue 4", exampleBytes1, 1800, - availableDates1, "2018-09-02", false, 0.85543f), - new Venue(19, "Venue 19", exampleBytes2, 6300, - availableDates2, "2019-01-15", true, 0.98716f), - new Venue(42, "Venue 42", exampleBytes3, 3000, - availableDates3, "2018-10-01", false, 0.72598f)); + new Venue( + 4, "Venue 4", exampleBytes1, 1800, availableDates1, "2018-09-02", false, 0.85543f), + new Venue( + 19, "Venue 19", exampleBytes2, 6300, availableDates2, "2019-01-15", true, 0.98716f), + new Venue( + 42, "Venue 42", exampleBytes3, 3000, availableDates3, "2018-10-01", false, 0.72598f)); // [END spanner_insert_datatypes_data] // [START spanner_create_database] @@ -329,7 +339,8 @@ static void deleteExampleData(DatabaseClient dbClient) { // [START spanner_query_data] static void query(DatabaseClient dbClient) { - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() // Execute a single read or query against Cloud Spanner. .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { while (resultSet.next()) { @@ -342,7 +353,8 @@ static void query(DatabaseClient dbClient) { // [START spanner_read_data] static void read(DatabaseClient dbClient) { - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() .read( "Albums", @@ -462,7 +474,8 @@ static void queryMarketingBudget(DatabaseClient dbClient) { // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to // null. A try-with-resource block is used to automatically release resources held by // ResultSet. - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() .executeQuery(Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums"))) { while (resultSet.next()) { @@ -534,7 +547,8 @@ static void queryUsingIndex(DatabaseClient dbClient) { // [START spanner_read_data_with_index] static void readUsingIndex(DatabaseClient dbClient) { - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() .readUsingIndex( "Albums", @@ -578,7 +592,8 @@ static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseId dbId) { // [START spanner_read_data_with_storing_index] static void readStoringIndex(DatabaseClient dbClient) { // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget. - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() .readUsingIndex( "Albums", @@ -624,10 +639,11 @@ static void readOnlyTransaction(DatabaseClient dbClient) { // [START spanner_read_stale_data] static void readStaleData(DatabaseClient dbClient) { - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse(TimestampBound.ofExactStaleness(15, TimeUnit.SECONDS)) .read( - "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "MarketingBudget"))) { + "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "MarketingBudget"))) { while (resultSet.next()) { System.out.printf( "%d %d %s\n", @@ -704,7 +720,8 @@ static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to // null. A try-with-resource block is used to automatically release resources held by // ResultSet. - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() .executeQuery( Statement.of( @@ -725,7 +742,8 @@ static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) { // [END spanner_query_data_with_timestamp_column] static void querySingersTable(DatabaseClient dbClient) { - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() .executeQuery(Statement.of("SELECT SingerId, FirstName, LastName FROM Singers"))) { while (resultSet.next()) { @@ -742,7 +760,8 @@ static void queryPerformancesTable(DatabaseClient dbClient) { // Rows without an explicit value for Revenue will have a Revenue equal to // null. A try-with-resource block is used to automatically release resources held by // ResultSet. - try (ResultSet resultSet = dbClient + try (ResultSet resultSet = + dbClient .singleUse() .executeQuery( Statement.of( @@ -1012,8 +1031,7 @@ public Void run(TransactionContext transaction) throws Exception { while (resultSet.next()) { System.out.printf( "%s %s\n", - resultSet.getString("FirstName"), - resultSet.getString("LastName")); + resultSet.getString("FirstName"), resultSet.getString("LastName")); } } return null; @@ -1239,15 +1257,24 @@ static void writeDatatypesData(DatabaseClient dbClient) { for (Venue venue : VENUES) { mutations.add( Mutation.newInsertBuilder("Venues") - .set("VenueId").to(venue.venueId) - .set("VenueName").to(venue.venueName) - .set("VenueInfo").to(venue.venueInfo) - .set("Capacity").to(venue.capacity) - .set("AvailableDates").to(venue.availableDates) - .set("LastContactDate").to(venue.lastContactDate) - .set("OutdoorVenue").to(venue.outdoorVenue) - .set("PopularityScore").to(venue.popularityScore) - .set("LastUpdateTime").to(Value.COMMIT_TIMESTAMP) + .set("VenueId") + .to(venue.venueId) + .set("VenueName") + .to(venue.venueName) + .set("VenueInfo") + .to(venue.venueInfo) + .set("Capacity") + .to(venue.capacity) + .set("AvailableDates") + .to(venue.availableDates) + .set("LastContactDate") + .to(venue.lastContactDate) + .set("OutdoorVenue") + .to(venue.outdoorVenue) + .set("PopularityScore") + .to(venue.popularityScore) + .set("LastUpdateTime") + .to(Value.COMMIT_TIMESTAMP) .build()); } dbClient.write(mutations); @@ -1256,9 +1283,8 @@ static void writeDatatypesData(DatabaseClient dbClient) { // [START spanner_query_with_array_parameter] static void queryWithArray(DatabaseClient dbClient) { - Value exampleArray = Value.dateArray(Arrays.asList( - Date.parseDate("2020-10-01"), - Date.parseDate("2020-11-01"))); + Value exampleArray = + Value.dateArray(Arrays.asList(Date.parseDate("2020-10-01"), Date.parseDate("2020-11-01"))); Statement statement = Statement.newBuilder( @@ -1304,21 +1330,18 @@ static void queryWithBool(DatabaseClient dbClient) { // [START spanner_query_with_bytes_parameter] static void queryWithBytes(DatabaseClient dbClient) { - ByteArray exampleBytes = ByteArray.fromBase64( - BaseEncoding.base64().encode("Hello World 1".getBytes())); + ByteArray exampleBytes = + ByteArray.fromBase64(BaseEncoding.base64().encode("Hello World 1".getBytes())); Statement statement = Statement.newBuilder( - "SELECT VenueId, VenueName FROM Venues " - + "WHERE VenueInfo = @venueInfo") + "SELECT VenueId, VenueName FROM Venues " + "WHERE VenueInfo = @venueInfo") .bind("venueInfo") .to(exampleBytes) .build(); try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { while (resultSet.next()) { System.out.printf( - "%d %s\n", - resultSet.getLong("VenueId"), - resultSet.getString("VenueName")); + "%d %s\n", resultSet.getLong("VenueId"), resultSet.getString("VenueName")); } } } @@ -1373,8 +1396,7 @@ static void queryWithInt(DatabaseClient dbClient) { long exampleInt = 3000; Statement statement = Statement.newBuilder( - "SELECT VenueId, VenueName, Capacity FROM Venues " - + "WHERE Capacity >= @capacity") + "SELECT VenueId, VenueName, Capacity FROM Venues " + "WHERE Capacity >= @capacity") .bind("capacity") .to(exampleInt) .build(); @@ -1395,17 +1417,14 @@ static void queryWithString(DatabaseClient dbClient) { String exampleString = "Venue 42"; Statement statement = Statement.newBuilder( - "SELECT VenueId, VenueName FROM Venues " - + "WHERE VenueName = @venueName") + "SELECT VenueId, VenueName FROM Venues " + "WHERE VenueName = @venueName") .bind("venueName") .to(exampleString) .build(); try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) { while (resultSet.next()) { System.out.printf( - "%d %s\n", - resultSet.getLong("VenueId"), - resultSet.getString("VenueName")); + "%d %s\n", resultSet.getLong("VenueId"), resultSet.getString("VenueName")); } } } @@ -1726,7 +1745,7 @@ public static void main(String[] args) throws Exception { // Use client here... // [END init_client] run(dbClient, dbAdminClient, command, db); - // [START init_client] + // [START init_client] } finally { spanner.close(); } diff --git a/samples/snippets/src/main/java/com/example/spanner/TracingSample.java b/samples/snippets/src/main/java/com/example/spanner/TracingSample.java index 4dfd5a1cb15..ddadf62a26e 100644 --- a/samples/snippets/src/main/java/com/example/spanner/TracingSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/TracingSample.java @@ -22,7 +22,6 @@ import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; - import io.opencensus.common.Scope; import io.opencensus.contrib.grpc.metrics.RpcViews; import io.opencensus.contrib.zpages.ZPageHandlers; @@ -30,14 +29,11 @@ import io.opencensus.exporter.trace.stackdriver.StackdriverExporter; import io.opencensus.trace.Tracing; import io.opencensus.trace.samplers.Samplers; - import java.util.Arrays; -/** - * This sample demonstrates how to enable opencensus tracing and stats in cloud spanner client. - */ +/** This sample demonstrates how to enable opencensus tracing and stats in cloud spanner client. */ public class TracingSample { - + private static final String SAMPLE_SPAN = "CloudSpannerSample"; public static void main(String[] args) throws Exception { @@ -52,25 +48,27 @@ public static void main(String[] args) throws Exception { ZPageHandlers.startHttpServerAndRegisterAll(8080); // Installs an exporter for stack driver traces. StackdriverExporter.createAndRegister(); - Tracing.getExportComponent().getSampledSpanStore().registerSpanNamesForCollection( - Arrays.asList(SAMPLE_SPAN)); + Tracing.getExportComponent() + .getSampledSpanStore() + .registerSpanNamesForCollection(Arrays.asList(SAMPLE_SPAN)); // Installs an exporter for stack driver stats. StackdriverStatsExporter.createAndRegister(); RpcViews.registerAllCumulativeViews(); - + // Name of your instance & database. String instanceId = args[0]; String databaseId = args[1]; try { // Creates a database client - DatabaseClient dbClient = spanner.getDatabaseClient(DatabaseId.of( - options.getProjectId(), instanceId, databaseId)); + DatabaseClient dbClient = + spanner.getDatabaseClient(DatabaseId.of(options.getProjectId(), instanceId, databaseId)); // Queries the database - try (Scope ss = Tracing.getTracer() - .spanBuilderWithExplicitParent(SAMPLE_SPAN, null) - .setSampler(Samplers.alwaysSample()) - .startScopedSpan()) { + try (Scope ss = + Tracing.getTracer() + .spanBuilderWithExplicitParent(SAMPLE_SPAN, null) + .setSampler(Samplers.alwaysSample()) + .startScopedSpan()) { ResultSet resultSet = dbClient.singleUse().executeQuery(Statement.of("SELECT 1")); System.out.println("\n\nResults:"); @@ -84,5 +82,4 @@ public static void main(String[] args) throws Exception { spanner.close(); } } - } From 7a9ada0eb5dcb829bbd5c059949bf25ff8277479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Sat, 28 Mar 2020 04:12:36 +0100 Subject: [PATCH 32/79] samples: feat: add backups samples (#2487) Add samples for Spanner backups. --- .../com/example/spanner/SpannerSample.java | 372 +++++++++++++++++- .../com/example/spanner/SpannerSampleIT.java | 108 +++++ 2 files changed, 474 insertions(+), 6 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index ffffeeac68b..6639ada47b9 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -20,16 +20,28 @@ import static com.google.cloud.spanner.Type.StructField; import com.google.api.gax.longrunning.OperationFuture; +import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.paging.Page; +import com.google.api.gax.retrying.RetryingFuture; +import com.google.api.gax.rpc.StatusCode; import com.google.cloud.ByteArray; import com.google.cloud.Date; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Backup; +import com.google.cloud.spanner.BackupId; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Instance; +import com.google.cloud.spanner.InstanceAdminClient; +import com.google.cloud.spanner.InstanceId; import com.google.cloud.spanner.Key; import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Options; import com.google.cloud.spanner.ReadOnlyTransaction; +import com.google.cloud.spanner.RestoreInfo; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerBatchUpdateException; @@ -43,16 +55,24 @@ import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Value; import com.google.common.io.BaseEncoding; +import com.google.longrunning.Operation; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.spanner.admin.database.v1.CreateBackupMetadata; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; +import com.google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata; +import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; -import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import org.threeten.bp.LocalDate; +import org.threeten.bp.LocalDateTime; +import org.threeten.bp.OffsetDateTime; +import org.threeten.bp.temporal.ChronoField; /** * Example code for using the Cloud Spanner API. This example demonstrates all the common operations @@ -148,6 +168,17 @@ static class Venue { } } + /** Get a database id to restore a backup to from the sample database id. */ + static String createRestoredSampleDbId(DatabaseId database) { + int index = database.getDatabase().indexOf('-'); + String prefix = database.getDatabase().substring(0, index); + String restoredDbId = database.getDatabase().replace(prefix, "restored"); + if (restoredDbId.length() > 30) { + restoredDbId = restoredDbId.substring(0, 30); + } + return restoredDbId; + } + // [START spanner_insert_data] static final List SINGERS = Arrays.asList( @@ -1432,8 +1463,7 @@ static void queryWithString(DatabaseClient dbClient) { // [START spanner_query_with_timestamp_parameter] static void queryWithTimestampParameter(DatabaseClient dbClient) { - Timestamp timestamp = new Timestamp(System.currentTimeMillis()); - Instant exampleTimestamp = timestamp.toInstant(); + Instant exampleTimestamp = Instant.now(); Statement statement = Statement.newBuilder( "SELECT VenueId, VenueName, LastUpdateTime FROM Venues " @@ -1492,11 +1522,298 @@ static void queryWithQueryOptions(DatabaseClient dbClient) { } // [END spanner_query_with_query_options] + // [START spanner_create_backup] + static void createBackup( + DatabaseAdminClient dbAdminClient, DatabaseId databaseId, BackupId backupId) { + // Set expire time to 14 days from now. + Timestamp expireTime = Timestamp.ofTimeMicroseconds(TimeUnit.MICROSECONDS.convert( + System.currentTimeMillis() + TimeUnit.DAYS.toMillis(14), TimeUnit.MILLISECONDS)); + Backup backup = + dbAdminClient + .newBackupBuilder(backupId) + .setDatabase(databaseId) + .setExpireTime(expireTime) + .build(); + // Initiate the request which returns an OperationFuture. + System.out.println("Creating backup [" + backup.getId() + "]..."); + OperationFuture op = backup.create(); + try { + // Wait for the backup operation to complete. + backup = op.get(); + System.out.println("Created backup [" + backup.getId() + "]"); + } catch (ExecutionException e) { + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + + // Reload the metadata of the backup from the server. + backup = backup.reload(); + System.out.println( + String.format( + "Backup %s of size %d bytes was created at %s", + backup.getId().getName(), + backup.getSize(), + LocalDateTime.ofEpochSecond( + backup.getProto().getCreateTime().getSeconds(), + backup.getProto().getCreateTime().getNanos(), + OffsetDateTime.now().getOffset())).toString()); + } + // [END spanner_create_backup] + + // [START spanner_cancel_create_backup] + static void cancelCreateBackup( + DatabaseAdminClient dbAdminClient, DatabaseId databaseId, BackupId backupId) { + // Set expire time to 14 days from now. + Timestamp expireTime = Timestamp.ofTimeMicroseconds(TimeUnit.MICROSECONDS.convert( + System.currentTimeMillis() + TimeUnit.DAYS.toMillis(14), TimeUnit.MILLISECONDS)); + + // Create a backup instance. + Backup backup = + dbAdminClient + .newBackupBuilder(backupId) + .setDatabase(databaseId) + .setExpireTime(expireTime) + .build(); + // Start the creation of a backup. + System.out.println("Creating backup [" + backup.getId() + "]..."); + OperationFuture op = backup.create(); + try { + // Try to cancel the backup operation. + System.out.println("Cancelling create backup operation for [" + backup.getId() + "]..."); + dbAdminClient.cancelOperation(op.getName()); + // Get a polling future for the running operation. This future will regularly poll the server + // for the current status of the backup operation. + RetryingFuture pollingFuture = op.getPollingFuture(); + // Wait for the operation to finish. + // isDone will return true when the operation is complete, regardless of whether it was + // successful or not. + while (!pollingFuture.get().isDone()) { + System.out.println("Waiting for the cancelled backup operation to finish..."); + Thread.sleep(TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS)); + } + if (pollingFuture.get().getErrorCode() == null) { + // Backup was created before it could be cancelled. Delete the backup. + backup.delete(); + } else if (pollingFuture.get().getErrorCode().getCode() == StatusCode.Code.CANCELLED) { + System.out.println("Backup operation for [" + backup.getId() + "] successfully cancelled"); + } + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_cancel_create_backup] + + // [START spanner_list_backup_operations] + static void listBackupOperations(InstanceAdminClient instanceAdminClient, DatabaseId databaseId) { + Instance instance = instanceAdminClient.getInstance(databaseId.getInstanceId().getInstance()); + // Get create backup operations for the sample database. + String filter = + String.format( + "(metadata.database:%s) AND " + + "(metadata.@type:type.googleapis.com/" + + "google.spanner.admin.database.v1.CreateBackupMetadata)", + databaseId.getName()); + Page operations = instance.listBackupOperations(Options.filter(filter)); + for (Operation op : operations.iterateAll()) { + try { + CreateBackupMetadata metadata = op.getMetadata().unpack(CreateBackupMetadata.class); + System.out.println( + String.format( + "Backup %s on database %s pending: %d%% complete", + metadata.getName(), + metadata.getDatabase(), + metadata.getProgress().getProgressPercent())); + } catch (InvalidProtocolBufferException e) { + // The returned operation does not contain CreateBackupMetadata. + System.err.println(e.getMessage()); + } + } + } + // [END spanner_list_backup_operations] + + // [START spanner_list_database_operations] + static void listDatabaseOperations( + InstanceAdminClient instanceAdminClient, + DatabaseAdminClient dbAdminClient, + InstanceId instanceId) { + Instance instance = instanceAdminClient.getInstance(instanceId.getInstance()); + // Get optimize restored database operations. + String filter = "(metadata.@type:type.googleapis.com/" + + "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)"; + for (Operation op : instance.listDatabaseOperations(Options.filter(filter)).iterateAll()) { + try { + OptimizeRestoredDatabaseMetadata metadata = + op.getMetadata().unpack(OptimizeRestoredDatabaseMetadata.class); + System.out.println(String.format( + "Database %s restored from backup is %d%% optimized", + metadata.getName(), + metadata.getProgress().getProgressPercent())); + } catch (InvalidProtocolBufferException e) { + // The returned operation does not contain OptimizeRestoredDatabaseMetadata. + System.err.println(e.getMessage()); + } + } + } + // [END spanner_list_database_operations] + + // [START spanner_list_backups] + static void listBackups( + InstanceAdminClient instanceAdminClient, DatabaseId databaseId, BackupId backupId) { + Instance instance = instanceAdminClient.getInstance(databaseId.getInstanceId().getInstance()); + // List all backups. + System.out.println("All backups:"); + for (Backup backup : instance.listBackups().iterateAll()) { + System.out.println(backup); + } + + // List all backups with a specific name. + System.out.println( + String.format("All backups with backup name containing \"%s\":", backupId.getBackup())); + for (Backup backup : instance.listBackups( + Options.filter(String.format("name:%s", backupId.getBackup()))).iterateAll()) { + System.out.println(backup); + } + + // List all backups for databases whose name contains a certain text. + System.out.println( + String.format( + "All backups for databases with a name containing \"%s\":", + databaseId.getDatabase())); + for (Backup backup : instance.listBackups( + Options.filter(String.format("database:%s", databaseId.getDatabase()))).iterateAll()) { + System.out.println(backup); + } + + // List all backups that expire before a certain time. + Timestamp expireTime = Timestamp.ofTimeMicroseconds(TimeUnit.MICROSECONDS.convert( + System.currentTimeMillis() + TimeUnit.DAYS.toMillis(30), TimeUnit.MILLISECONDS)); + System.out.println(String.format("All backups that expire before %s:", expireTime.toString())); + for (Backup backup : + instance.listBackups( + Options.filter(String.format("expire_time < \"%s\"", expireTime.toString()))) + .iterateAll()) { + System.out.println(backup); + } + + // List all backups with size greater than a certain number of bytes. + System.out.println("All backups with size greater than 100 bytes:"); + for (Backup backup : instance.listBackups(Options.filter("size_bytes > 100")).iterateAll()) { + System.out.println(backup); + } + + // List all backups with a create time after a certain timestamp and that are also ready. + Timestamp createTime = Timestamp.ofTimeMicroseconds(TimeUnit.MICROSECONDS.convert( + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1), TimeUnit.MILLISECONDS)); + System.out.println( + String.format( + "All databases created after %s and that are ready:", createTime.toString())); + for (Backup backup : + instance + .listBackups(Options.filter( + String.format("create_time >= \"%s\" AND state:READY", createTime.toString()))) + .iterateAll()) { + System.out.println(backup); + } + + // List backups using pagination. + System.out.println("All backups, listed using pagination:"); + Page page = instance.listBackups(Options.pageSize(10)); + while (true) { + for (Backup backup : page.getValues()) { + System.out.println(backup); + } + if (!page.hasNextPage()) { + break; + } + page = page.getNextPage(); + } + } + // [END spanner_list_backups] + + // [START spanner_restore_backup] + static void restoreBackup( + DatabaseAdminClient dbAdminClient, + BackupId backupId, + DatabaseId sourceDatabaseId, + DatabaseId restoreToDatabase) { + Backup backup = dbAdminClient.newBackupBuilder(backupId).build(); + // Initiate the request which returns an OperationFuture. + System.out.println(String.format( + "Restoring backup [%s] to database [%s]...", + backup.getId().toString(), + restoreToDatabase.toString())); + try { + OperationFuture op = backup.restore(restoreToDatabase); + // Wait until the database has been restored. + Database db = op.get(); + // Refresh database metadata and get the restore info. + RestoreInfo restore = db.reload().getRestoreInfo(); + System.out.println( + "Restored database [" + + restore.getSourceDatabase().getName() + + "] from [" + + restore.getBackup().getName() + + "]"); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + // [END spanner_restore_backup] + + // [START spanner_update_backup] + static void updateBackup(DatabaseAdminClient dbAdminClient, BackupId backupId) { + // Get current backup metadata. + Backup backup = dbAdminClient.newBackupBuilder(backupId).build().reload(); + // Add 30 days to the expire time. + // Expire time must be within 366 days of the create time of the backup. + Timestamp expireTime = + Timestamp.ofTimeMicroseconds( + TimeUnit.SECONDS.toMicros(backup.getExpireTime().getSeconds()) + + TimeUnit.NANOSECONDS.toMicros(backup.getExpireTime().getNanos()) + + TimeUnit.DAYS.toMicros(30L)); + System.out.println(String.format( + "Updating expire time of backup [%s] to %s...", + backupId.toString(), + LocalDateTime.ofEpochSecond( + expireTime.getSeconds(), + expireTime.getNanos(), + OffsetDateTime.now().getOffset()).toString())); + + // Update expire time. + backup = backup.toBuilder().setExpireTime(expireTime).build(); + backup.updateExpireTime(); + System.out.println("Updated backup [" + backupId + "]"); + } + // [END spanner_update_backup] + + // [START spanner_delete_backup] + static void deleteBackup(DatabaseAdminClient dbAdminClient, BackupId backupId) { + Backup backup = dbAdminClient.newBackupBuilder(backupId).build(); + // Delete the backup. + System.out.println("Deleting backup [" + backupId + "]..."); + backup.delete(); + // Verify that the backup is deleted. + if (backup.exists()) { + System.out.println("Delete backup [" + backupId + "] failed"); + throw new RuntimeException("Delete backup [" + backupId + "] failed"); + } else { + System.out.println("Deleted backup [" + backupId + "]"); + } + } + // [END spanner_delete_backup] + static void run( DatabaseClient dbClient, DatabaseAdminClient dbAdminClient, + InstanceAdminClient instanceAdminClient, String command, - DatabaseId database) { + DatabaseId database, + BackupId backup) { switch (command) { case "createdatabase": createDatabase(dbAdminClient, database); @@ -1654,6 +1971,37 @@ static void run( case "querywithqueryoptions": queryWithQueryOptions(dbClient); break; + case "createbackup": + createBackup(dbAdminClient, database, backup); + break; + case "cancelcreatebackup": + cancelCreateBackup( + dbAdminClient, + database, + BackupId.of(backup.getInstanceId(), backup.getBackup() + "_cancel")); + break; + case "listbackupoperations": + listBackupOperations(instanceAdminClient, database); + break; + case "listdatabaseoperations": + listDatabaseOperations(instanceAdminClient, dbAdminClient, database.getInstanceId()); + break; + case "listbackups": + listBackups(instanceAdminClient, database, backup); + break; + case "restorebackup": + restoreBackup( + dbAdminClient, + backup, + database, + DatabaseId.of(database.getInstanceId(), createRestoredSampleDbId(database))); + break; + case "updatebackup": + updateBackup(dbAdminClient, backup); + break; + case "deletebackup": + deleteBackup(dbAdminClient, backup); + break; default: printUsageAndExit(); } @@ -1716,6 +2064,11 @@ static void printUsageAndExit() { System.err.println(" SpannerExample querywithtimestampparameter my-instance example-db"); System.err.println(" SpannerExample clientwithqueryoptions my-instance example-db"); System.err.println(" SpannerExample querywithqueryoptions my-instance example-db"); + System.err.println(" SpannerExample createbackup my-instance example-db"); + System.err.println(" SpannerExample listbackups my-instance example-db"); + System.err.println(" SpannerExample listbackupoperations my-instance example-db"); + System.err.println(" SpannerExample listdatabaseoperations my-instance example-db"); + System.err.println(" SpannerExample restorebackup my-instance example-db"); System.exit(1); } @@ -1739,13 +2092,20 @@ public static void main(String[] args) throws Exception { + clientProject); printUsageAndExit(); } + // Generate a backup id for the sample database. + String backupName = + String.format( + "%s_%02d", + db.getDatabase(), LocalDate.now().get(ChronoField.ALIGNED_WEEK_OF_YEAR)); + BackupId backup = BackupId.of(db.getInstanceId(), backupName); + // [START init_client] DatabaseClient dbClient = spanner.getDatabaseClient(db); DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + InstanceAdminClient instanceAdminClient = spanner.getInstanceAdminClient(); // Use client here... // [END init_client] - run(dbClient, dbAdminClient, command, db); - // [START init_client] + run(dbClient, dbAdminClient, instanceAdminClient, command, db, backup); } finally { spanner.close(); } diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 0bf458ee201..5360f2f1d13 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -18,18 +18,28 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.cloud.spanner.BackupId; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerOptions; +import com.google.common.base.CharMatcher; +import com.google.common.util.concurrent.Uninterruptibles; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.threeten.bp.LocalDate; +import org.threeten.bp.temporal.ChronoField; /** Unit tests for {@code SpannerSample} */ @RunWith(JUnit4.class) @@ -59,11 +69,15 @@ public void setUp() throws Exception { dbClient = spanner.getDatabaseAdminClient(); dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + dbClient.dropDatabase( + dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); } @After public void tearDown() throws Exception { dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + dbClient.dropDatabase( + dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); } @Test @@ -241,6 +255,100 @@ public void testSample() throws Exception { assertThat(out).contains("1 1 Total Junk"); out = runSample("querywithqueryoptions"); assertThat(out).contains("1 1 Total Junk"); + + String backupName = + String.format( + "%s_%02d", + dbId.getDatabase(), LocalDate.now().get(ChronoField.ALIGNED_WEEK_OF_YEAR)); + BackupId backupId = BackupId.of(dbId.getInstanceId(), backupName); + + out = runSample("createbackup"); + assertThat(out).contains("Created backup [" + backupId + "]"); + + out = runSample("cancelcreatebackup"); + assertThat(out).contains( + "Backup operation for [" + backupId + "_cancel] successfully cancelled"); + + out = runSample("listbackupoperations"); + assertThat(out).contains( + String.format( + "Backup %s on database %s pending:", + backupId.getName(), + dbId.getName())); + + out = runSample("listbackups"); + assertThat(out).contains("All backups:"); + assertThat(out).contains( + String.format("All backups with backup name containing \"%s\":", backupId.getBackup())); + assertThat(out).contains(String.format( + "All backups for databases with a name containing \"%s\":", + dbId.getDatabase())); + assertThat(out).contains( + String.format("All backups that expire before")); + assertThat(out).contains("All backups with size greater than 100 bytes:"); + assertThat(out).containsMatch( + Pattern.compile("All databases created after (.+) and that are ready:")); + assertThat(out).contains("All backups, listed using pagination:"); + // All the above tests should include the created backup exactly once, i.e. exactly 7 times. + assertThat(countOccurrences(out, backupId.getName())).isEqualTo(7); + + // Try the restore operation in a retry loop, as there is a limit on the number of restore + // operations that is allowed to execute simultaneously, and we should retry if we hit this + // limit. + int restoreAttempts = 0; + while (true) { + try { + out = runSample("restorebackup"); + assertThat(out).contains( + "Restored database [" + + dbId.getName() + + "] from [" + + backupId.getName() + + "]"); + break; + } catch (SpannerException e) { + if (e.getErrorCode() == ErrorCode.FAILED_PRECONDITION + && e.getMessage() + .contains("Please retry the operation once the pending restores complete")) { + restoreAttempts++; + if (restoreAttempts == 10) { + System.out.println( + "Restore operation failed 10 times because of other pending restores. " + + "Giving up restore."); + break; + } + Uninterruptibles.sleepUninterruptibly(60L, TimeUnit.SECONDS); + } else { + throw e; + } + } + } + + out = runSample("listdatabaseoperations"); + assertThat(out).contains( + String.format( + "Database %s restored from backup", + DatabaseId.of( + dbId.getInstanceId(), + SpannerSample.createRestoredSampleDbId(dbId)) + .getName())); + + out = runSample("updatebackup"); + assertThat(out).contains( + String.format("Updated backup [" + backupId + "]")); + + // Drop the restored database before we try to delete the backup. + // Otherwise the delete backup operation might fail as the backup is still in use by + // the OptimizeRestoredDatabase operation. + dbClient.dropDatabase( + dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); + + out = runSample("deletebackup"); + assertThat(out).contains("Deleted backup [" + backupId + "]"); + } + + private static int countOccurrences(String input, String search) { + return input.split(search).length - 1; } private String formatForTest(String name) { From 77d8cf5d55f20331c6ea9d99ff4548cfc316d79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 3 Jun 2020 20:33:19 +0200 Subject: [PATCH 33/79] samples: feat: add sample for createInstance (#3036) * feat: add sample for createInstance * fix: fix formatting issues + add missing static keyword * fix: fix broken integration test * Update spanner/cloud-client/src/main/java/com/example/spanner/CreateInstanceExample.java Co-authored-by: Averi Kitsch Co-authored-by: Averi Kitsch --- .../spanner/CreateInstanceExample.java | 71 +++++++++++++++++++ .../com/example/spanner/SpannerSampleIT.java | 62 ++++++++++++---- 2 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java diff --git a/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java b/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java new file mode 100644 index 00000000000..fe7e6ec4248 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_create_instance] +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.spanner.Instance; +import com.google.cloud.spanner.InstanceAdminClient; +import com.google.cloud.spanner.InstanceConfigId; +import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.InstanceInfo; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; +import java.util.concurrent.ExecutionException; + +class CreateInstanceExample { + + static void createInstance() { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + createInstance(projectId, instanceId); + } + + static void createInstance(String projectId, String instanceId) { + Spanner spanner = SpannerOptions.newBuilder().setProjectId(projectId).build().getService(); + InstanceAdminClient instanceAdminClient = spanner.getInstanceAdminClient(); + + // Set Instance configuration. + String configId = "regional-us-central1"; + int nodeCount = 2; + String displayName = "Descriptive name"; + + // Create an InstanceInfo object that will be used to create the instance. + InstanceInfo instanceInfo = + InstanceInfo.newBuilder(InstanceId.of(projectId, instanceId)) + .setInstanceConfigId(InstanceConfigId.of(projectId, configId)) + .setNodeCount(nodeCount) + .setDisplayName(displayName) + .build(); + OperationFuture operation = + instanceAdminClient.createInstance(instanceInfo); + try { + // Wait for the createInstance operation to finish. + Instance instance = operation.get(); + System.out.printf("Instance %s was successfully created%n", instance.getId()); + } catch (ExecutionException e) { + System.out.printf( + "Error: Creating instance %s failed with error message %s%n", + instanceInfo.getId(), e.getMessage()); + } catch (InterruptedException e) { + System.out.println("Error: Waiting for createInstance operation to finish was interrupted"); + } + } +} +//[END spanner_create_instance] diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 5360f2f1d13..21a307fe422 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -22,19 +22,18 @@ import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.InstanceId; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerOptions; -import com.google.common.base.CharMatcher; import com.google.common.util.concurrent.Uninterruptibles; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.junit.After; -import org.junit.Before; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -46,10 +45,12 @@ @SuppressWarnings("checkstyle:abbreviationaswordinname") public class SpannerSampleIT { // The instance needs to exist for tests to pass. - private final String instanceId = System.getProperty("spanner.test.instance"); - private final String databaseId = formatForTest(System.getProperty("spanner.sample.database")); - DatabaseId dbId; - DatabaseAdminClient dbClient; + private static final String instanceId = System.getProperty("spanner.test.instance"); + private static final String databaseId = + formatForTest(System.getProperty("spanner.sample.database")); + static Spanner spanner; + static DatabaseId dbId; + static DatabaseAdminClient dbClient; private long lastUpdateDataTimeInMillis; private String runSample(String command) throws Exception { @@ -62,10 +63,10 @@ private String runSample(String command) throws Exception { return bout.toString(); } - @Before - public void setUp() throws Exception { + @BeforeClass + public static void setUp() throws Exception { SpannerOptions options = SpannerOptions.newBuilder().build(); - Spanner spanner = options.getService(); + spanner = options.getService(); dbClient = spanner.getDatabaseAdminClient(); dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); @@ -73,8 +74,8 @@ public void setUp() throws Exception { dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); } - @After - public void tearDown() throws Exception { + @AfterClass + public static void tearDown() throws Exception { dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); dbClient.dropDatabase( dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); @@ -347,11 +348,44 @@ public void testSample() throws Exception { assertThat(out).contains("Deleted backup [" + backupId + "]"); } + private String runSampleRunnable(Runnable sample) { + PrintStream stdOut = System.out; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + sample.run(); + System.setOut(stdOut); + return bout.toString(); + } + + @Test + public void testCreateInstanceSample() { + String instanceId = formatForTest("sample-inst"); + String out = + runSampleRunnable( + new Runnable() { + @Override + public void run() { + try { + CreateInstanceExample.createInstance( + dbId.getInstanceId().getProject(), instanceId); + } finally { + spanner.getInstanceAdminClient().deleteInstance(instanceId); + } + } + }); + assertThat(out) + .contains( + String.format( + "Instance %s was successfully created", + InstanceId.of(dbId.getInstanceId().getProject(), instanceId))); + } + private static int countOccurrences(String input, String search) { return input.split(search).length - 1; } - private String formatForTest(String name) { + private static String formatForTest(String name) { return name + "-" + UUID.randomUUID().toString().substring(0, 20); } } From 3023d4fe8240aa5ceb15eb31c5ca640dd9603a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 4 Jun 2020 17:38:04 +0200 Subject: [PATCH 34/79] samples: fix: include different ways to delete data in sample (#3034) --- .../com/example/spanner/SpannerSample.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 6639ada47b9..0c4e0a28b85 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -37,6 +37,7 @@ import com.google.cloud.spanner.InstanceAdminClient; import com.google.cloud.spanner.InstanceId; import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeyRange; import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.Options; @@ -353,15 +354,21 @@ static void writeExampleData(DatabaseClient dbClient) { static void deleteExampleData(DatabaseClient dbClient) { List mutations = new ArrayList<>(); - // KeySet.all() can be used to delete all the rows in a table. - mutations.add(Mutation.delete("Albums", KeySet.all())); + // KeySet.Builder can be used to delete a specific set of rows. + // Delete the Albums with the key values (2,1) and (2,3). + mutations.add( + Mutation.delete( + "Albums", KeySet.newBuilder().addKey(Key.of(2, 1)).addKey(Key.of(2, 3)).build())); - // KeySet.singleKey() can be used to delete one row at a time. - for (Singer singer : SINGERS) { - mutations.add( - Mutation.delete( - "Singers", KeySet.singleKey(Key.newBuilder().append(singer.singerId).build()))); - } + // KeyRange can be used to delete rows with a key in a specific range. + // Delete a range of rows where the column key is >=3 and <5 + mutations.add( + Mutation.delete("Singers", KeySet.range(KeyRange.closedOpen(Key.of(3), Key.of(5))))); + + // KeySet.all() can be used to delete all the rows in a table. + // Delete remaining Singers rows, which will also delete the remaining Albums rows since it was + // defined with ON DELETE CASCADE. + mutations.add(Mutation.delete("Singers", KeySet.all())); dbClient.write(mutations); System.out.printf("Records deleted.\n"); From 9d91154e69c365efdf1a4417228b2a14cc2867cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 30 Jul 2020 20:18:09 +0200 Subject: [PATCH 35/79] samples: docs: add samples for async api (#3429) * docs: add samples for async api * fix: lint errors * fix: place entire transaction in try block * fix: keep runner in a variable to comply with code example in docs * fix: move both runners to a variable + reformat * docs: add tags --- .../com/example/spanner/AsyncDmlExample.java | 74 +++++ .../example/spanner/AsyncQueryExample.java | 107 +++++++ .../spanner/AsyncQueryToListAsyncExample.java | 88 ++++++ .../com/example/spanner/AsyncReadExample.java | 111 +++++++ .../AsyncReadOnlyTransactionExample.java | 135 ++++++++ .../example/spanner/AsyncReadRowExample.java | 84 +++++ .../spanner/AsyncReadUsingIndexExample.java | 112 +++++++ .../example/spanner/AsyncRunnerExample.java | 137 ++++++++ .../AsyncTransactionManagerExample.java | 147 +++++++++ .../com/example/spanner/AsyncExamplesIT.java | 299 ++++++++++++++++++ 10 files changed, 1294 insertions(+) create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncQueryExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncReadExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncReadOnlyTransactionExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncReadRowExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncReadUsingIndexExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncRunnerExample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/AsyncTransactionManagerExample.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/AsyncExamplesIT.java diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java new file mode 100644 index 00000000000..f2e53d7a223 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_async_dml_standard_insert] +import com.google.api.core.ApiFuture; +import com.google.cloud.spanner.AsyncRunner; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.TransactionContext; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; + +class AsyncDmlExample { + + static void asyncDml() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncDml(client); + } + } + + // Execute a DML statement asynchronously. + static void asyncDml(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncRunner runner = client.runAsync(); + ApiFuture rowCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES " + + "(12, 'Melissa', 'Garcia'), " + + "(13, 'Russell', 'Morales'), " + + "(14, 'Jacqueline', 'Long'), " + + "(15, 'Dylan', 'Shaw')"; + return txn.executeUpdateAsync(Statement.of(sql)); + } + }, + executor); + System.out.printf("%d records inserted.%n", rowCount.get()); + executor.shutdown(); + } +} +//[END spanner_async_dml_standard_insert] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncQueryExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryExample.java new file mode 100644 index 00000000000..257ea6e4ad7 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryExample.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_async_query_data] +import com.google.api.core.ApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncQueryExample { + + static void asyncQuery() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncQuery(client); + } + } + + // Execute a query asynchronously and process the results in a callback. + static void asyncQuery(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ApiFuture finished; + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (AsyncResultSet resultSet = + client + .singleUse() + .executeQueryAsync(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { + // Setting a callback will automatically start the iteration over the results of the query + // using the specified executor. The callback will be called at least once. The returned + // ApiFuture is done when the callback has returned DONE and all resources used by the + // AsyncResultSet have been released. + finished = + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + // OK: There is a row ready. + case OK: + System.out.printf( + "%d %d %s%n", + resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + break; + + // DONE: There are no more rows in the result set. + case DONE: + return CallbackResponse.DONE; + + // NOT_READY: There are currently no more rows in the buffer. + case NOT_READY: + return CallbackResponse.CONTINUE; + + default: + throw new IllegalStateException(); + } + } + } catch (SpannerException e) { + System.out.printf("Error in callback: %s%n", e.getMessage()); + return CallbackResponse.DONE; + } + } + }); + } + + // This ApiFuture is done when the callback has returned DONE and all resources of the + // asynchronous result set have been released. + finished.get(30L, TimeUnit.SECONDS); + executor.shutdown(); + } +} +//[END spanner_async_query_data] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java new file mode 100644 index 00000000000..78397d62fe6 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_async_query_to_list] +import com.google.api.core.ApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncQueryToListAsyncExample { + static class Album { + final long singerId; + final long albumId; + final String albumTitle; + + Album(long singerId, long albumId, String albumTitle) { + this.singerId = singerId; + this.albumId = albumId; + this.albumTitle = albumTitle; + } + } + + static void asyncQueryToList() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncQueryToList(client); + } + } + + // Execute a query asynchronously and transform the result to a list. + static void asyncQueryToList(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ApiFuture> albums; + try (AsyncResultSet resultSet = + client + .singleUse() + .executeQueryAsync(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { + // Convert the result set to a list of Albums asynchronously. + albums = + resultSet.toListAsync( + reader -> { + return new Album( + reader.getLong("SingerId"), + reader.getLong("AlbumId"), + reader.getString("AlbumTitle")); + }, + executor); + } + + for (Album album : albums.get(30L, TimeUnit.SECONDS)) { + System.out.printf("%d %d %s%n", album.singerId, album.albumId, album.albumTitle); + } + executor.shutdown(); + } +} +//[END spanner_async_query_to_list] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncReadExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncReadExample.java new file mode 100644 index 00000000000..26fefb0df99 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncReadExample.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_async_read_data] +import com.google.api.core.ApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncReadExample { + + static void asyncRead() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncRead(client); + } + } + + // Execute a query asynchronously and process the results in a callback. + static void asyncRead(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ApiFuture finished; + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (AsyncResultSet resultSet = + client + .singleUse() + .readAsync( + "Albums", + KeySet.all(), // Read all rows in a table. + Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) { + // Setting a callback will automatically start the iteration over the results of the query + // using the specified executor. The callback will be called at least once. The returned + // ApiFuture is done when the callback has returned DONE and all resources used by the + // AsyncResultSet have been released. + finished = + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + // OK: There is a row ready. + case OK: + System.out.printf( + "%d %d %s%n", + resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + break; + + // DONE: There are no more rows in the result set. + case DONE: + return CallbackResponse.DONE; + + // NOT_READY: There are currently no more rows in the buffer. + case NOT_READY: + return CallbackResponse.CONTINUE; + + default: + throw new IllegalStateException(); + } + } + } catch (SpannerException e) { + System.out.printf("Error in callback: %s%n", e.getMessage()); + return CallbackResponse.DONE; + } + } + }); + } + + // This ApiFuture is done when the callback has returned DONE and all resources of the + // asynchronous result set have been released. + finished.get(30L, TimeUnit.SECONDS); + executor.shutdown(); + } +} +//[END spanner_async_read_data] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncReadOnlyTransactionExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncReadOnlyTransactionExample.java new file mode 100644 index 00000000000..dc2560ab784 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncReadOnlyTransactionExample.java @@ -0,0 +1,135 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_async_read_only_transaction] +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ReadOnlyTransaction; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncReadOnlyTransactionExample { + + static void asyncReadOnlyTransaction() + throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncReadOnlyTransaction(client); + } + } + + static void asyncReadOnlyTransaction(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ApiFuture finished1; + ApiFuture finished2; + ExecutorService executor = Executors.newFixedThreadPool(2); + + try (ReadOnlyTransaction transaction = client.readOnlyTransaction()) { + try (AsyncResultSet resultSet = + transaction.executeQueryAsync( + Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) { + finished1 = + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case OK: + System.out.printf( + "%d %d %s%n", + resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2)); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + default: + throw new IllegalStateException(); + } + } + } catch (SpannerException e) { + System.out.printf("Error in callback: %s%n", e.getMessage()); + return CallbackResponse.DONE; + } + } + }); + } + try (AsyncResultSet resultSet = + transaction.executeQueryAsync( + Statement.of("SELECT SingerId, FirstName, LastName FROM Singers"))) { + finished2 = + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case OK: + System.out.printf( + "%d %s %s%n", + resultSet.getLong(0), + resultSet.getString(1), + resultSet.getString(2)); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + default: + throw new IllegalStateException(); + } + } + } catch (SpannerException e) { + System.out.printf("Error in callback: %s%n", e.getMessage()); + return CallbackResponse.DONE; + } + } + }); + } + } + + ApiFutures.allAsList(ImmutableList.of(finished1, finished2)).get(60L, TimeUnit.SECONDS); + executor.shutdown(); + } +} +//[END spanner_async_read_only_transaction] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncReadRowExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncReadRowExample.java new file mode 100644 index 00000000000..f05f5c12cff --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncReadRowExample.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_async_read_row] +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Struct; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncReadRowExample { + + static void asyncReadRow() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncReadRow(client); + } + } + + // Read a single row asynchronously and print out the result when available. + static void asyncReadRow(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ApiFuture row = + client + .singleUse() + .readRowAsync( + "Albums", Key.of(1L, 1L), Arrays.asList("SingerId", "AlbumId", "AlbumTitle")); + // Add a callback that will print out the contents of the row when the result has been returned. + SettableApiFuture printed = SettableApiFuture.create(); + ApiFutures.addCallback( + row, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + System.out.printf("Error reading row: %s%n", t.getMessage()); + printed.set(null); + } + + @Override + public void onSuccess(Struct result) { + System.out.printf( + "%d %d %s%n", result.getLong(0), result.getLong(1), result.getString(2)); + printed.set(null); + } + }, + MoreExecutors.directExecutor()); + + // Wait until the row has been printed. + printed.get(30L, TimeUnit.SECONDS); + } +} +//[END spanner_async_read_row] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncReadUsingIndexExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncReadUsingIndexExample.java new file mode 100644 index 00000000000..ffb3b9cb21c --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncReadUsingIndexExample.java @@ -0,0 +1,112 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_async_read_data_with_index] +import com.google.api.core.ApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncReadUsingIndexExample { + + static void asyncReadUsingIndex() + throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncReadUsingIndex(client); + } + } + + // Execute a query asynchronously and process the results in a callback. + static void asyncReadUsingIndex(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ApiFuture finished; + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (AsyncResultSet resultSet = + client + .singleUse() + .readUsingIndexAsync( + "Albums", + "AlbumsByAlbumTitle", + KeySet.all(), // Read all rows in a table. + Arrays.asList("AlbumId", "AlbumTitle"))) { + // Setting a callback will automatically start the iteration over the results of the query + // using the specified executor. The callback will be called at least once. The returned + // ApiFuture is done when the callback has returned DONE and all resources used by the + // AsyncResultSet have been released. + finished = + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + // OK: There is a row ready. + case OK: + System.out.printf( + "%d %s%n", resultSet.getLong(0), resultSet.getString(1)); + break; + + // DONE: There are no more rows in the result set. + case DONE: + return CallbackResponse.DONE; + + // NOT_READY: There are currently no more rows in the buffer. + case NOT_READY: + return CallbackResponse.CONTINUE; + + default: + throw new IllegalStateException(); + } + } + } catch (SpannerException e) { + System.out.printf("Error in callback: %s%n", e.getMessage()); + return CallbackResponse.DONE; + } + } + }); + } + + // This ApiFuture is done when the callback has returned DONE and all resources of the + // asynchronous result set have been released. + finished.get(30L, TimeUnit.SECONDS); + executor.shutdown(); + } +} +//[END spanner_async_read_data_with_index] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncRunnerExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncRunnerExample.java new file mode 100644 index 00000000000..575164e610d --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncRunnerExample.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_async_read_write_transaction] +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.cloud.spanner.AsyncRunner; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TransactionContext; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncRunnerExample { + + static void asyncRunner() throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncRunner(client); + } + } + + // Execute a read/write transaction asynchronously. + static void asyncRunner(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + // Create an async transaction runner. + AsyncRunner runner = client.runAsync(); + // The transaction returns the total number of rows that were updated as a future array of + // longs. + ApiFuture rowCounts = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + // Transfer marketing budget from one album to another. We do it in a + // transaction to ensure that the transfer is atomic. + ApiFuture album1BudgetFut = + txn.readRowAsync("Albums", Key.of(1, 1), ImmutableList.of("MarketingBudget")); + ApiFuture album2BudgetFut = + txn.readRowAsync("Albums", Key.of(2, 2), ImmutableList.of("MarketingBudget")); + + try { + // Transaction will only be committed if this condition still holds at the + // time of commit. Otherwise it will be aborted and the AsyncWork will be + // rerun by the client library. + long transfer = 200_000; + if (album2BudgetFut.get().getLong(0) >= transfer) { + long album1Budget = album1BudgetFut.get().getLong(0); + long album2Budget = album2BudgetFut.get().getLong(0); + + album1Budget += transfer; + album2Budget -= transfer; + Statement updateStatement1 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 1 and AlbumId = 1") + .bind("AlbumBudget") + .to(album1Budget) + .build(); + Statement updateStatement2 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 2 and AlbumId = 2") + .bind("AlbumBudget") + .to(album2Budget) + .build(); + return txn.batchUpdateAsync( + ImmutableList.of(updateStatement1, updateStatement2)); + } else { + return ApiFutures.immediateFuture(new long[] {0L, 0L}); + } + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + }, + executor); + + ApiFuture totalUpdateCount = + ApiFutures.transform( + rowCounts, + new ApiFunction() { + @SuppressFBWarnings("UVA_USE_VAR_ARGS") + @Override + public Long apply(long[] input) { + return Arrays.stream(input).sum(); + } + }, + MoreExecutors.directExecutor()); + System.out.printf("%d records updated.%n", totalUpdateCount.get(30L, TimeUnit.SECONDS)); + executor.shutdown(); + } +} +//[END spanner_async_read_write_transaction] diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncTransactionManagerExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncTransactionManagerExample.java new file mode 100644 index 00000000000..5d4087a2fab --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncTransactionManagerExample.java @@ -0,0 +1,147 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_async_transaction_manager] +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.cloud.spanner.AbortedException; +import com.google.cloud.spanner.AsyncTransactionManager; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; +import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class AsyncTransactionManagerExample { + + static void asyncTransactionManager() + throws InterruptedException, ExecutionException, TimeoutException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + asyncTransactionManager(client); + } + } + + static void asyncTransactionManager(DatabaseClient client) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + AsyncTransactionStep, long[]> updateCounts; + try (AsyncTransactionManager mgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + // Loop to retry aborted errors. + while (true) { + try { + updateCounts = + txn.then( + (transaction, v) -> { + // Execute two reads in parallel and return the result of these as the input + // for the next step of the transaction. + ApiFuture album1BudgetFut = + transaction.readRowAsync( + "Albums", Key.of(1, 1), ImmutableList.of("MarketingBudget")); + ApiFuture album2BudgetFut = + transaction.readRowAsync( + "Albums", Key.of(2, 2), ImmutableList.of("MarketingBudget")); + return ApiFutures.allAsList(Arrays.asList(album1BudgetFut, album2BudgetFut)); + }, + executor) + // The input of the next step of the transaction is the return value of the + // previous step, i.e. a list containing the marketing budget of two Albums. + .then( + (transaction, budgets) -> { + long album1Budget = budgets.get(0).getLong(0); + long album2Budget = budgets.get(1).getLong(0); + long transfer = 200_000; + if (album2Budget >= transfer) { + album1Budget += transfer; + album2Budget -= transfer; + Statement updateStatement1 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 1 and AlbumId = 1") + .bind("AlbumBudget") + .to(album1Budget) + .build(); + Statement updateStatement2 = + Statement.newBuilder( + "UPDATE Albums " + + "SET MarketingBudget = @AlbumBudget " + + "WHERE SingerId = 2 and AlbumId = 2") + .bind("AlbumBudget") + .to(album2Budget) + .build(); + return transaction.batchUpdateAsync( + ImmutableList.of(updateStatement1, updateStatement2)); + } else { + return ApiFutures.immediateFuture(new long[] {0L, 0L}); + } + }, + executor); + // Commit after the updates. + CommitTimestampFuture commitTsFut = updateCounts.commitAsync(); + // Wait for the transaction to finish and execute a retry if necessary. + commitTsFut.get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + + // Calculate the total update count. + ApiFuture totalUpdateCount = + ApiFutures.transform( + updateCounts, + new ApiFunction() { + @SuppressFBWarnings("UVA_USE_VAR_ARGS") + @Override + public Long apply(long[] input) { + return Arrays.stream(input).sum(); + } + }, + MoreExecutors.directExecutor()); + System.out.printf("%d records updated.%n", totalUpdateCount.get(30L, TimeUnit.SECONDS)); + executor.shutdown(); + } +} +//[END spanner_async_transaction_manager] diff --git a/samples/snippets/src/test/java/com/example/spanner/AsyncExamplesIT.java b/samples/snippets/src/test/java/com/example/spanner/AsyncExamplesIT.java new file mode 100644 index 00000000000..b164fd8b4f4 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/AsyncExamplesIT.java @@ -0,0 +1,299 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Instance; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for Cloud Spanner Async API examples. */ +@RunWith(JUnit4.class) +public class AsyncExamplesIT { + // The instance needs to exist for tests to pass. + private static String instanceId = System.getProperty("spanner.test.instance"); + private static String databaseId = + formatForTest(System.getProperty("spanner.sample.database", "mysample")); + private static DatabaseId dbId; + private static DatabaseAdminClient dbClient; + private static Spanner spanner; + + private interface AsyncRunnable { + public void run() throws InterruptedException, ExecutionException, TimeoutException; + } + + private String runExample(AsyncRunnable example) + throws InterruptedException, ExecutionException, TimeoutException { + PrintStream stdOut = System.out; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + example.run(); + System.setOut(stdOut); + return bout.toString(); + } + + @BeforeClass + public static void createTestDatabase() throws Exception { + SpannerOptions options = SpannerOptions.newBuilder().build(); + spanner = options.getService(); + dbClient = spanner.getDatabaseAdminClient(); + if (instanceId == null) { + Iterator iterator = + spanner.getInstanceAdminClient().listInstances().iterateAll().iterator(); + if (iterator.hasNext()) { + instanceId = iterator.next().getId().getInstance(); + } + } + dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + dbClient + .createDatabase( + instanceId, + databaseId, + ImmutableList.of( + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)," + + " MarketingBudget INT64" + + ") PRIMARY KEY (SingerId, AlbumId)," + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE", + "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)")) + .get(); + } + + @AfterClass + public static void dropTestDatabase() throws Exception { + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + spanner.close(); + } + + static class Singer { + final long singerId; + final String firstName; + final String lastName; + + Singer(long singerId, String firstName, String lastName) { + this.singerId = singerId; + this.firstName = firstName; + this.lastName = lastName; + } + } + + static class Album { + final long singerId; + final long albumId; + final String albumTitle; + final Long marketingBudget; + + Album(long singerId, long albumId, String albumTitle, Long marketingBudget) { + this.singerId = singerId; + this.albumId = albumId; + this.albumTitle = albumTitle; + this.marketingBudget = marketingBudget; + } + } + + private static final List TEST_SINGERS = + Arrays.asList( + new Singer(1, "Marc", "Richards"), + new Singer(2, "Catalina", "Smith"), + new Singer(3, "Alice", "Trentor"), + new Singer(4, "Lea", "Martin"), + new Singer(5, "David", "Lomond")); + private static final List ALBUMS = + Arrays.asList( + new Album(1, 1, "Total Junk", 300_000L), + new Album(1, 2, "Go, Go, Go", 400_000L), + new Album(2, 1, "Green", 150_000L), + new Album(2, 2, "Forever Hold Your Peace", 350_000L), + new Album(2, 3, "Terrified", null)); + + @Before + public void insertTestData() { + DatabaseClient client = spanner.getDatabaseClient(dbId); + ImmutableList.Builder mutations = + ImmutableList.builderWithExpectedSize(TEST_SINGERS.size()); + for (Singer singer : TEST_SINGERS) { + mutations.add( + Mutation.newInsertBuilder("Singers") + .set("SingerId") + .to(singer.singerId) + .set("FirstName") + .to(singer.firstName) + .set("LastName") + .to(singer.lastName) + .build()); + } + for (Album album : ALBUMS) { + mutations.add( + Mutation.newInsertBuilder("Albums") + .set("SingerId") + .to(album.singerId) + .set("AlbumId") + .to(album.albumId) + .set("AlbumTitle") + .to(album.albumTitle) + .set("MarketingBudget") + .to(album.marketingBudget) + .build()); + } + client.write(mutations.build()); + } + + private void assertSingersOutput(String out) { + assertThat(out).contains("1 Marc Richard"); + assertThat(out).contains("2 Catalina Smith"); + assertThat(out).contains("3 Alice Trentor"); + assertThat(out).contains("4 Lea Martin"); + assertThat(out).contains("5 David Lomond"); + } + + private void assertAlbumsOutput(String out) { + assertThat(out).contains("1 1 Total Junk"); + assertThat(out).contains("1 2 Go, Go, Go"); + assertThat(out).contains("2 1 Green"); + assertThat(out).contains("2 2 Forever Hold Your Peace"); + assertThat(out).contains("2 3 Terrified"); + } + + @After + public void removeTestData() { + DatabaseClient client = spanner.getDatabaseClient(dbId); + client.write(Arrays.asList(Mutation.delete("Singers", KeySet.all()))); + } + + @Test + public void asyncQuery_shouldReturnData() + throws InterruptedException, ExecutionException, TimeoutException { + String out = runExample(() -> AsyncQueryExample.asyncQuery(spanner.getDatabaseClient(dbId))); + assertAlbumsOutput(out); + } + + @Test + public void asyncQueryToListAsync_shouldReturnData() + throws InterruptedException, ExecutionException, TimeoutException { + String out = + runExample( + () -> AsyncQueryToListAsyncExample.asyncQueryToList(spanner.getDatabaseClient(dbId))); + assertAlbumsOutput(out); + } + + @Test + public void asyncRead_shouldReturnData() + throws InterruptedException, ExecutionException, TimeoutException { + String out = runExample(() -> AsyncReadExample.asyncRead(spanner.getDatabaseClient(dbId))); + assertAlbumsOutput(out); + } + + @Test + public void asyncReadUsingIndex_shouldReturnDataInCorrectOrder() + throws InterruptedException, ExecutionException, TimeoutException { + String out = + runExample( + () -> AsyncReadUsingIndexExample.asyncReadUsingIndex(spanner.getDatabaseClient(dbId))); + assertThat(out) + .contains( + "2 Forever Hold Your Peace\n" + + "2 Go, Go, Go\n" + + "1 Green\n" + + "3 Terrified\n" + + "1 Total Junk"); + } + + @Test + public void asyncReadOnlyTransaction_shouldReturnData() + throws InterruptedException, ExecutionException, TimeoutException { + String out = + runExample( + () -> + AsyncReadOnlyTransactionExample.asyncReadOnlyTransaction( + spanner.getDatabaseClient(dbId))); + assertAlbumsOutput(out); + assertSingersOutput(out); + } + + @Test + public void asyncDml_shouldInsertRows() + throws InterruptedException, ExecutionException, TimeoutException { + String out = runExample(() -> AsyncDmlExample.asyncDml(spanner.getDatabaseClient(dbId))); + assertThat(out).contains("4 records inserted."); + } + + @Test + public void asyncRunner_shouldUpdateRows() + throws InterruptedException, ExecutionException, TimeoutException { + String out = runExample(() -> AsyncRunnerExample.asyncRunner(spanner.getDatabaseClient(dbId))); + assertThat(out).contains("2 records updated."); + } + + @Test + public void asyncTransactionManager_shouldUpdateRows() + throws InterruptedException, ExecutionException, TimeoutException { + String out = + runExample( + () -> + AsyncTransactionManagerExample.asyncTransactionManager( + spanner.getDatabaseClient(dbId))); + assertThat(out).contains("2 records updated."); + } + + @Test + public void asyncReadRow_shouldPrintRow() + throws InterruptedException, ExecutionException, TimeoutException { + String out = + runExample(() -> AsyncReadRowExample.asyncReadRow(spanner.getDatabaseClient(dbId))); + assertThat(out).contains("1 1 Total Junk"); + assertThat(out).doesNotContain("1 2 Go, Go, Go"); + assertThat(out).doesNotContain("2 1 Green"); + assertThat(out).doesNotContain("2 2 Forever Hold Your Peace"); + assertThat(out).doesNotContain("2 3 Terrified"); + } + + static String formatForTest(String name) { + return name + "-" + UUID.randomUUID().toString().substring(0, 20); + } +} From f928fd5680e4a79be588e22d14d88d998137f5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Sun, 2 Aug 2020 01:21:09 +0200 Subject: [PATCH 36/79] samples: docs: add sample for setting timeout and retry settings (#3445) * docs: add sample for setting timeout and retry settings * fix: move start tag * docs: explain why retry settings use a separate chain --- .../CustomTimeoutAndRetrySettingsExample.java | 88 +++++++++++++++ .../spanner/SpannerStandaloneExamplesIT.java | 106 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java diff --git a/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java b/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java new file mode 100644 index 00000000000..dac0da5306f --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +//[START spanner_set_custom_timeout_and_retry] +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.TransactionContext; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import org.threeten.bp.Duration; + +class CustomTimeoutAndRetrySettingsExample { + + static void executeSqlWithCustomTimeoutAndRetrySettings() { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + executeSqlWithCustomTimeoutAndRetrySettings(projectId, instanceId, databaseId); + } + + // Create a Spanner client with custom ExecuteSql timeout and retry settings. + static void executeSqlWithCustomTimeoutAndRetrySettings( + String projectId, String instanceId, String databaseId) { + SpannerOptions.Builder builder = SpannerOptions.newBuilder().setProjectId(projectId); + // Set custom timeout and retry settings for the ExecuteSql RPC. + // This must be done in a separate chain as the setRetryableCodes and setRetrySettings methods + // return a UnaryCallSettings.Builder instead of a SpannerOptions.Builder. + builder + .getSpannerStubSettingsBuilder() + .executeSqlSettings() + // Configure which errors should be retried. + .setRetryableCodes(Code.DEADLINE_EXCEEDED, Code.UNAVAILABLE) + .setRetrySettings( + RetrySettings.newBuilder() + // Configure retry delay settings. + .setInitialRetryDelay(Duration.ofMillis(500)) + .setMaxRetryDelay(Duration.ofSeconds(64)) + .setRetryDelayMultiplier(1.5) + + // Configure RPC and total timeout settings. + .setInitialRpcTimeout(Duration.ofSeconds(60)) + .setMaxRpcTimeout(Duration.ofSeconds(60)) + .setRpcTimeoutMultiplier(1.0) + .setTotalTimeout(Duration.ofSeconds(60)) + .build()); + // Create a Spanner client using the custom retry and timeout settings. + try (Spanner spanner = builder.build().getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + String sql = + "INSERT Singers (SingerId, FirstName, LastName)\n" + + "VALUES (20, 'George', 'Washington')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record inserted.%n", rowCount); + return null; + } + }); + } + } +} +// [END spanner_set_custom_timeout_and_retry] diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java new file mode 100644 index 00000000000..6808278ce4d --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Instance; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Iterator; +import java.util.UUID; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for Cloud Spanner cloud client examples. */ +@RunWith(JUnit4.class) +public class SpannerStandaloneExamplesIT { + // The instance needs to exist for tests to pass. + private static String instanceId = System.getProperty("spanner.test.instance"); + private static String databaseId = + formatForTest(System.getProperty("spanner.sample.database", "mysample")); + private static DatabaseId dbId; + private static DatabaseAdminClient dbClient; + private static Spanner spanner; + + private String runExample(Runnable example) { + PrintStream stdOut = System.out; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + example.run(); + System.setOut(stdOut); + return bout.toString(); + } + + @BeforeClass + public static void createTestDatabase() throws Exception { + SpannerOptions options = SpannerOptions.newBuilder().build(); + spanner = options.getService(); + dbClient = spanner.getDatabaseAdminClient(); + if (instanceId == null) { + Iterator iterator = + spanner.getInstanceAdminClient().listInstances().iterateAll().iterator(); + if (iterator.hasNext()) { + instanceId = iterator.next().getId().getInstance(); + } + } + dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + dbClient + .createDatabase( + instanceId, + databaseId, + ImmutableList.of( + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)" + + ") PRIMARY KEY (SingerId)")) + .get(); + } + + @AfterClass + public static void dropTestDatabase() throws Exception { + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + spanner.close(); + } + + @Test + public void executeSqlWithCustomTimeoutAndRetrySettings_shouldWriteData() { + String projectId = spanner.getOptions().getProjectId(); + String out = + runExample( + () -> + CustomTimeoutAndRetrySettingsExample.executeSqlWithCustomTimeoutAndRetrySettings( + projectId, instanceId, databaseId)); + assertThat(out).contains("1 record inserted."); + } + + static String formatForTest(String name) { + return name + "-" + UUID.randomUUID().toString().substring(0, 20); + } +} From ed0665c71abbce57a28cb79531783145eccab1fb Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Thu, 6 Aug 2020 10:49:47 +0200 Subject: [PATCH 37/79] fix: fixes sample tests - add missing dependencies for tracing sample - create test instance when none is set - id may not end with a hyphen - restored DB id must also not end with a hyphen - use formatted name for quick start test - fix build errors - updates async example to use java.util.List instead of ImmutableList --- samples/install-without-bom/pom.xml | 57 +++++++++ samples/snapshot/pom.xml | 57 +++++++++ samples/snippets/pom.xml | 57 ++++++++- .../spanner/AsyncQueryToListAsyncExample.java | 5 +- .../spanner/CreateInstanceExample.java | 56 ++++----- .../com/example/spanner/SpannerSample.java | 3 + .../example/spanner/QuickstartSampleIT.java | 50 ++++++-- .../com/example/spanner/SpannerSampleIT.java | 109 +++++++++++------- 8 files changed, 314 insertions(+), 80 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index d4cca25d3ca..4744333966b 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -21,6 +21,9 @@ 1.8 1.8 UTF-8 + 0.26.0 + 1.1.0 + 1.100.1 @@ -32,6 +35,60 @@ 2.0.1 + + + io.opencensus + opencensus-api + ${opencensus.version} + + + io.opencensus + opencensus-impl + ${opencensus.version} + runtime + + + io.opencensus + opencensus-contrib-zpages + ${opencensus.version} + + + io.opencensus + opencensus-exporter-trace-stackdriver + ${opencensus.version} + + + com.google.cloud + google-cloud-trace + + + + + io.opencensus + opencensus-exporter-stats-stackdriver + ${opencensus.version} + + + com.google.cloud + google-cloud-monitoring + + + + + io.opencensus + opencensus-contrib-grpc-metrics + ${opencensus.version} + + + com.google.cloud + google-cloud-trace + ${trace.version} + + + com.google.cloud + google-cloud-monitoring + ${cloudmonitoring.version} + junit diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 0bce44187ee..8b6362d3111 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -21,6 +21,9 @@ 1.8 1.8 UTF-8 + 0.26.0 + 1.1.0 + 1.100.1 @@ -31,6 +34,60 @@ 2.0.3-SNAPSHOT + + + io.opencensus + opencensus-api + ${opencensus.version} + + + io.opencensus + opencensus-impl + ${opencensus.version} + runtime + + + io.opencensus + opencensus-contrib-zpages + ${opencensus.version} + + + io.opencensus + opencensus-exporter-trace-stackdriver + ${opencensus.version} + + + com.google.cloud + google-cloud-trace + + + + + io.opencensus + opencensus-exporter-stats-stackdriver + ${opencensus.version} + + + com.google.cloud + google-cloud-monitoring + + + + + io.opencensus + opencensus-contrib-grpc-metrics + ${opencensus.version} + + + com.google.cloud + google-cloud-trace + ${trace.version} + + + com.google.cloud + google-cloud-monitoring + ${cloudmonitoring.version} + junit diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index bb111adc0ee..b717da4d088 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -21,6 +21,7 @@ 1.8 1.8 UTF-8 + 0.26.0 @@ -43,7 +44,61 @@ google-cloud-spanner - + + + io.opencensus + opencensus-api + ${opencensus.version} + + + io.opencensus + opencensus-impl + ${opencensus.version} + runtime + + + io.opencensus + opencensus-contrib-zpages + ${opencensus.version} + + + io.opencensus + opencensus-exporter-trace-stackdriver + ${opencensus.version} + + + com.google.cloud + google-cloud-trace + + + + + io.opencensus + opencensus-exporter-stats-stackdriver + ${opencensus.version} + + + com.google.cloud + google-cloud-monitoring + + + + + io.opencensus + opencensus-contrib-grpc-metrics + ${opencensus.version} + + + + com.google.cloud + google-cloud-trace + + + + com.google.cloud + google-cloud-monitoring + + junit junit diff --git a/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java index 78397d62fe6..76b3a123524 100644 --- a/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java +++ b/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java @@ -17,6 +17,7 @@ package com.example.spanner; // [START spanner_async_query_to_list] + import com.google.api.core.ApiFuture; import com.google.cloud.spanner.AsyncResultSet; import com.google.cloud.spanner.DatabaseClient; @@ -24,7 +25,7 @@ import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; -import com.google.common.collect.ImmutableList; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -62,7 +63,7 @@ static void asyncQueryToList() throws InterruptedException, ExecutionException, static void asyncQueryToList(DatabaseClient client) throws InterruptedException, ExecutionException, TimeoutException { ExecutorService executor = Executors.newSingleThreadExecutor(); - ApiFuture> albums; + ApiFuture> albums; try (AsyncResultSet resultSet = client .singleUse() diff --git a/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java b/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java index fe7e6ec4248..a455ac6f19f 100644 --- a/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java +++ b/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java @@ -16,7 +16,7 @@ package com.example.spanner; -//[START spanner_create_instance] +// [START spanner_create_instance] import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.spanner.Instance; import com.google.cloud.spanner.InstanceAdminClient; @@ -38,34 +38,36 @@ static void createInstance() { } static void createInstance(String projectId, String instanceId) { - Spanner spanner = SpannerOptions.newBuilder().setProjectId(projectId).build().getService(); - InstanceAdminClient instanceAdminClient = spanner.getInstanceAdminClient(); + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + InstanceAdminClient instanceAdminClient = spanner.getInstanceAdminClient(); - // Set Instance configuration. - String configId = "regional-us-central1"; - int nodeCount = 2; - String displayName = "Descriptive name"; + // Set Instance configuration. + String configId = "regional-us-central1"; + int nodeCount = 2; + String displayName = "Descriptive name"; - // Create an InstanceInfo object that will be used to create the instance. - InstanceInfo instanceInfo = - InstanceInfo.newBuilder(InstanceId.of(projectId, instanceId)) - .setInstanceConfigId(InstanceConfigId.of(projectId, configId)) - .setNodeCount(nodeCount) - .setDisplayName(displayName) - .build(); - OperationFuture operation = - instanceAdminClient.createInstance(instanceInfo); - try { - // Wait for the createInstance operation to finish. - Instance instance = operation.get(); - System.out.printf("Instance %s was successfully created%n", instance.getId()); - } catch (ExecutionException e) { - System.out.printf( - "Error: Creating instance %s failed with error message %s%n", - instanceInfo.getId(), e.getMessage()); - } catch (InterruptedException e) { - System.out.println("Error: Waiting for createInstance operation to finish was interrupted"); + // Create an InstanceInfo object that will be used to create the instance. + InstanceInfo instanceInfo = + InstanceInfo.newBuilder(InstanceId.of(projectId, instanceId)) + .setInstanceConfigId(InstanceConfigId.of(projectId, configId)) + .setNodeCount(nodeCount) + .setDisplayName(displayName) + .build(); + OperationFuture operation = + instanceAdminClient.createInstance(instanceInfo); + try { + // Wait for the createInstance operation to finish. + Instance instance = operation.get(); + System.out.printf("Instance %s was successfully created%n", instance.getId()); + } catch (ExecutionException e) { + System.out.printf( + "Error: Creating instance %s failed with error message %s%n", + instanceInfo.getId(), e.getMessage()); + } catch (InterruptedException e) { + System.out.println("Error: Waiting for createInstance operation to finish was interrupted"); + } } } } -//[END spanner_create_instance] +// [END spanner_create_instance] diff --git a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java index 0c4e0a28b85..ed2922aa16f 100644 --- a/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java @@ -177,6 +177,9 @@ static String createRestoredSampleDbId(DatabaseId database) { if (restoredDbId.length() > 30) { restoredDbId = restoredDbId.substring(0, 30); } + if (restoredDbId.endsWith("-")) { + restoredDbId = restoredDbId.substring(0, restoredDbId.length() - 1); + } return restoredDbId; } diff --git a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java index 707018d71c6..a032fb469bd 100644 --- a/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/QuickstartSampleIT.java @@ -18,29 +18,59 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.cloud.spanner.InstanceAdminClient; +import com.google.cloud.spanner.InstanceConfig; +import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.InstanceInfo; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.util.concurrent.ExecutionException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** - * Tests for quickstart sample. - */ +/** Tests for quickstart sample. */ @RunWith(JUnit4.class) -@SuppressWarnings("checkstyle:abbreviationaswordinname") public class QuickstartSampleIT { + private boolean ownedInstance = false; private String instanceId = System.getProperty("spanner.test.instance"); - // This database needs to exist for test to pass. - private String dbId = System.getProperty("spanner.quickstart.database"); + private String dbId = + System.getProperty("spanner.quickstart.database", SpannerSampleIT.formatForTest("sample")); private ByteArrayOutputStream bout; private PrintStream stdOut = System.out; private PrintStream out; @Before - public void setUp() { + public void setUp() throws SpannerException, InterruptedException, ExecutionException { + if (instanceId == null) { + instanceId = SpannerSampleIT.formatForTest("quick"); + SpannerOptions options = SpannerOptions.newBuilder().build(); + try (Spanner spanner = options.getService()) { + InstanceAdminClient instanceAdmin = spanner.getInstanceAdminClient(); + // Get first available instance config and create an instance. + InstanceConfig config = instanceAdmin.listInstanceConfigs().iterateAll().iterator().next(); + instanceAdmin + .createInstance( + InstanceInfo.newBuilder(InstanceId.of(options.getProjectId(), instanceId)) + .setDisplayName("samples-test") + .setInstanceConfigId(config.getId()) + .setNodeCount(1) + .build()) + .get(); + ownedInstance = true; + // Create a test database. + spanner + .getDatabaseAdminClient() + .createDatabase(instanceId, dbId, ImmutableList.of()) + .get(); + } + } bout = new ByteArrayOutputStream(); out = new PrintStream(bout); System.setOut(out); @@ -49,6 +79,12 @@ public void setUp() { @After public void tearDown() { System.setOut(stdOut); + if (ownedInstance) { + SpannerOptions options = SpannerOptions.newBuilder().build(); + try (Spanner spanner = options.getService()) { + spanner.getInstanceAdminClient().deleteInstance(instanceId); + } + } } @Test diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index 21a307fe422..a889ab97b20 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -18,11 +18,15 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.cloud.spanner.Backup; import com.google.cloud.spanner.BackupId; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.InstanceAdminClient; +import com.google.cloud.spanner.InstanceConfig; import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.InstanceInfo; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerOptions; @@ -42,12 +46,11 @@ /** Unit tests for {@code SpannerSample} */ @RunWith(JUnit4.class) -@SuppressWarnings("checkstyle:abbreviationaswordinname") public class SpannerSampleIT { - // The instance needs to exist for tests to pass. - private static final String instanceId = System.getProperty("spanner.test.instance"); + private static boolean ownedInstance = false; + private static String instanceId = System.getProperty("spanner.test.instance"); private static final String databaseId = - formatForTest(System.getProperty("spanner.sample.database")); + formatForTest(System.getProperty("spanner.sample.database", "my-sample")); static Spanner spanner; static DatabaseId dbId; static DatabaseAdminClient dbClient; @@ -67,6 +70,22 @@ private String runSample(String command) throws Exception { public static void setUp() throws Exception { SpannerOptions options = SpannerOptions.newBuilder().build(); spanner = options.getService(); + if (instanceId == null) { + instanceId = formatForTest("samples"); + InstanceAdminClient instanceAdmin = spanner.getInstanceAdminClient(); + // Get first available instance config and create an instance. + InstanceConfig config = instanceAdmin.listInstanceConfigs().iterateAll().iterator().next(); + instanceAdmin + .createInstance( + InstanceInfo.newBuilder(InstanceId.of(options.getProjectId(), instanceId)) + .setDisplayName("samples-test") + .setInstanceConfigId(config.getId()) + .setNodeCount(1) + .build()) + .get(); + ownedInstance = true; + } + dbClient = spanner.getDatabaseAdminClient(); dbId = DatabaseId.of(options.getProjectId(), instanceId, databaseId); dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); @@ -76,9 +95,16 @@ public static void setUp() throws Exception { @AfterClass public static void tearDown() throws Exception { - dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); - dbClient.dropDatabase( - dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); + if (ownedInstance) { + for (Backup backup : dbClient.listBackups(instanceId).iterateAll()) { + dbClient.deleteBackup(instanceId, backup.getId().getBackup()); + } + spanner.getInstanceAdminClient().deleteInstance(instanceId); + } else { + dbClient.dropDatabase(dbId.getInstanceId().getInstance(), dbId.getDatabase()); + dbClient.dropDatabase( + dbId.getInstanceId().getInstance(), SpannerSample.createRestoredSampleDbId(dbId)); + } } @Test @@ -259,36 +285,34 @@ public void testSample() throws Exception { String backupName = String.format( - "%s_%02d", - dbId.getDatabase(), LocalDate.now().get(ChronoField.ALIGNED_WEEK_OF_YEAR)); + "%s_%02d", dbId.getDatabase(), LocalDate.now().get(ChronoField.ALIGNED_WEEK_OF_YEAR)); BackupId backupId = BackupId.of(dbId.getInstanceId(), backupName); out = runSample("createbackup"); assertThat(out).contains("Created backup [" + backupId + "]"); out = runSample("cancelcreatebackup"); - assertThat(out).contains( - "Backup operation for [" + backupId + "_cancel] successfully cancelled"); + assertThat(out) + .contains("Backup operation for [" + backupId + "_cancel] successfully cancelled"); out = runSample("listbackupoperations"); - assertThat(out).contains( - String.format( - "Backup %s on database %s pending:", - backupId.getName(), - dbId.getName())); + assertThat(out) + .contains( + String.format("Backup %s on database %s pending:", backupId.getName(), dbId.getName())); out = runSample("listbackups"); assertThat(out).contains("All backups:"); - assertThat(out).contains( - String.format("All backups with backup name containing \"%s\":", backupId.getBackup())); - assertThat(out).contains(String.format( - "All backups for databases with a name containing \"%s\":", - dbId.getDatabase())); - assertThat(out).contains( - String.format("All backups that expire before")); + assertThat(out) + .contains( + String.format("All backups with backup name containing \"%s\":", backupId.getBackup())); + assertThat(out) + .contains( + String.format( + "All backups for databases with a name containing \"%s\":", dbId.getDatabase())); + assertThat(out).contains(String.format("All backups that expire before")); assertThat(out).contains("All backups with size greater than 100 bytes:"); - assertThat(out).containsMatch( - Pattern.compile("All databases created after (.+) and that are ready:")); + assertThat(out) + .containsMatch(Pattern.compile("All databases created after (.+) and that are ready:")); assertThat(out).contains("All backups, listed using pagination:"); // All the above tests should include the created backup exactly once, i.e. exactly 7 times. assertThat(countOccurrences(out, backupId.getName())).isEqualTo(7); @@ -300,12 +324,9 @@ public void testSample() throws Exception { while (true) { try { out = runSample("restorebackup"); - assertThat(out).contains( - "Restored database [" - + dbId.getName() - + "] from [" - + backupId.getName() - + "]"); + assertThat(out) + .contains( + "Restored database [" + dbId.getName() + "] from [" + backupId.getName() + "]"); break; } catch (SpannerException e) { if (e.getErrorCode() == ErrorCode.FAILED_PRECONDITION @@ -315,7 +336,7 @@ public void testSample() throws Exception { if (restoreAttempts == 10) { System.out.println( "Restore operation failed 10 times because of other pending restores. " - + "Giving up restore."); + + "Giving up restore."); break; } Uninterruptibles.sleepUninterruptibly(60L, TimeUnit.SECONDS); @@ -326,17 +347,15 @@ public void testSample() throws Exception { } out = runSample("listdatabaseoperations"); - assertThat(out).contains( - String.format( - "Database %s restored from backup", - DatabaseId.of( - dbId.getInstanceId(), - SpannerSample.createRestoredSampleDbId(dbId)) - .getName())); + assertThat(out) + .contains( + String.format( + "Database %s restored from backup", + DatabaseId.of(dbId.getInstanceId(), SpannerSample.createRestoredSampleDbId(dbId)) + .getName())); out = runSample("updatebackup"); - assertThat(out).contains( - String.format("Updated backup [" + backupId + "]")); + assertThat(out).contains(String.format("Updated backup [" + backupId + "]")); // Drop the restored database before we try to delete the backup. // Otherwise the delete backup operation might fail as the backup is still in use by @@ -385,7 +404,11 @@ private static int countOccurrences(String input, String search) { return input.split(search).length - 1; } - private static String formatForTest(String name) { - return name + "-" + UUID.randomUUID().toString().substring(0, 20); + static String formatForTest(String name) { + String res = name + "-" + UUID.randomUUID().toString().substring(0, 20); + if (res.endsWith("-")) { + res = res.substring(0, res.length() - 1); + } + return res; } } From 2ad0b7fc6d1369795702484181ee11ecf59a1f8b Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Fri, 2 Oct 2020 16:21:28 +1000 Subject: [PATCH 38/79] fix: ignores failing backup operations --- .../test/java/com/example/spanner/SpannerSampleIT.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java index a889ab97b20..62b23ea3404 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerSampleIT.java @@ -295,10 +295,11 @@ public void testSample() throws Exception { assertThat(out) .contains("Backup operation for [" + backupId + "_cancel] successfully cancelled"); - out = runSample("listbackupoperations"); - assertThat(out) - .contains( - String.format("Backup %s on database %s pending:", backupId.getName(), dbId.getName())); + // TODO: Re-enable this test once list backup operations bug is fixed: b/169431286 + // out = runSample("listbackupoperations"); + // assertThat(out).contains( + // String.format("Backup %s on database %s pending:", backupId.getName(), dbId.getName()) + // ); out = runSample("listbackups"); assertThat(out).contains("All backups:"); From fa6d9641b7b2a5bb1d00de6b99b0f8bc157245d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 6 Oct 2020 11:06:09 +0200 Subject: [PATCH 39/79] fix: increase visibility of #get() (#486) --- .../java/com/google/cloud/spanner/AbstractLazyInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java index c78a994c166..0b1a4b3fc28 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -27,7 +27,7 @@ public abstract class AbstractLazyInitializer { private volatile Exception error; /** Returns an initialized instance of T. */ - T get() throws Exception { + public T get() throws Exception { // First check without a lock to improve performance. if (!initialized) { synchronized (lock) { From 691a23c19635e264722737943425568db2a4c0d4 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 6 Oct 2020 22:08:22 +0200 Subject: [PATCH 40/79] chore(deps): update dependency com.google.cloud:libraries-bom to v12 (#499) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [com.google.cloud:libraries-bom](https://togithub.com/GoogleCloudPlatform/cloud-opensource-java) | major | `11.1.0` -> `12.0.0` | --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/snippets/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index b717da4d088..f612cbf457d 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -31,7 +31,7 @@ com.google.cloud libraries-bom - 11.1.0 + 12.0.0 pom import From 3dd0675d2d7882d40a6af1e12fda3b4617019870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 8 Oct 2020 16:08:50 +0200 Subject: [PATCH 41/79] feat!: async connection API (#392) * feat: support setting timeout per RPC The Spanner client allows a user to set custom timeouts while creating a SpannerOptions instance, but these timeouts are static and are applied to all invocations of the RPCs. This change introduces the possibility to set custom timeouts and other call options on a per-RPC basis. Fixes #378 * fix: change grpc deps from test to compile scope * feat: add async api for connection * fix: fix test failures * fix: move state handling from callback to callable * fix: fix integration tests with emulator * fix: fix timeout integration test on emulator * fix: prevent flakiness in DDL tests * fix: fix clirr build failures * fix: do not set transaction state for Aborted err * fix: set transaction state after retry * cleanup: remove sync methods and use async instead * cleanup: remove unused code * feat: make ddl async * fix: reduce timeout and remove debug info * feat: make runBatch async * test: set forkCount to 1 to investigate test failure * fix: linting + clirr * fix: prevent deadlock in DmlBatch * fix: fix DMLBatch state handling * tests: add tests for aborted async transactions * test: add aborted tests * fix: add change to clirr + more tests * fix: require a rollback after a tx has aborted * docs: add javadoc for new methods * tests: add integration tests * fix: wait for commit before select * fix: fix handling aborted commit * docs: document behavior -Async methods * fix: iterating without callback could cause exception * fix: remove todos and commented code * feat: keep track of caller to include in stacktrace * docs: explain why Aborted is active * fix: use ticker for better testability * test: increase coverage and remove unused code * test: add additional tests * docs: add missing @override * docs: fix comment --- .../clirr-ignored-differences.xml | 52 + .../cloud/spanner/AsyncResultSetImpl.java | 37 +- .../com/google/cloud/spanner/ErrorCode.java | 2 +- .../com/google/cloud/spanner/ResultSets.java | 40 +- .../cloud/spanner/SpannerApiFutures.java | 43 + .../spanner/SpannerExceptionFactory.java | 26 + .../cloud/spanner/TransactionRunnerImpl.java | 10 +- .../connection/AbstractBaseUnitOfWork.java | 144 +- .../AbstractMultiUseTransaction.java | 15 +- .../connection/AsyncStatementResult.java | 47 + .../connection/AsyncStatementResultImpl.java | 130 ++ .../spanner/connection/ChecksumResultSet.java | 18 +- .../cloud/spanner/connection/Connection.java | 247 +++- .../spanner/connection/ConnectionImpl.java | 276 ++-- .../spanner/connection/ConnectionOptions.java | 22 + .../cloud/spanner/connection/DdlBatch.java | 118 +- .../cloud/spanner/connection/DmlBatch.java | 68 +- .../connection/ReadOnlyTransaction.java | 27 +- .../connection/ReadWriteTransaction.java | 586 ++++---- .../connection/SingleUseTransaction.java | 391 +++--- .../cloud/spanner/connection/SpannerPool.java | 28 +- .../spanner/connection/StatementExecutor.java | 73 +- .../connection/StatementResultImpl.java | 23 + .../cloud/spanner/connection/UnitOfWork.java | 60 +- .../cloud/spanner/MockSpannerServiceImpl.java | 83 +- .../google/cloud/spanner/ResultSetsTest.java | 137 ++ .../cloud/spanner/SpannerApiFuturesTest.java | 118 ++ .../connection/AbstractMockServerTest.java | 63 +- .../AsyncStatementResultImplTest.java | 99 ++ .../ConnectionAsyncApiAbortedTest.java | 688 ++++++++++ .../connection/ConnectionAsyncApiTest.java | 833 ++++++++++++ .../connection/ConnectionImplTest.java | 24 +- .../spanner/connection/ConnectionTest.java | 77 ++ .../spanner/connection/DdlBatchTest.java | 141 +- .../spanner/connection/DmlBatchTest.java | 41 +- .../connection/ITAbstractSpannerTest.java | 16 + .../connection/ReadOnlyTransactionTest.java | 50 +- .../connection/ReadWriteTransactionTest.java | 41 +- .../connection/SingleUseTransactionTest.java | 165 +-- .../spanner/connection/SpannerPoolTest.java | 145 +- .../connection/StatementTimeoutTest.java | 1194 ++++++++--------- .../it/ITAsyncTransactionRetryTest.java | 1015 ++++++++++++++ .../connection/it/ITReadOnlySpannerTest.java | 17 +- .../connection/it/ITSqlMusicScriptTest.java | 2 + .../connection/it/ITTransactionRetryTest.java | 4 +- .../ITSqlScriptTest_TestStatementTimeout.sql | 16 +- 46 files changed, 5690 insertions(+), 1762 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResult.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResultImpl.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerApiFuturesTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AsyncStatementResultImplTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiAbortedTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITAsyncTransactionRetryTest.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index bc33de3bbbc..cfbcb88f852 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -319,4 +319,56 @@ com/google/cloud/spanner/Value java.util.List getNumericArray() + + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture beginTransactionAsync() + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture commitAsync() + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.cloud.spanner.connection.AsyncStatementResult executeAsync(com.google.cloud.spanner.Statement) + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture executeBatchUpdateAsync(java.lang.Iterable) + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture executeUpdateAsync(com.google.cloud.spanner.Statement) + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture rollbackAsync() + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture runBatchAsync() + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture writeAsync(com.google.cloud.spanner.Mutation) + + + 7012 + com/google/cloud/spanner/connection/Connection + com.google.api.core.ApiFuture writeAsync(java.lang.Iterable) + + + 7004 + com/google/cloud/spanner/ResultSets + com.google.cloud.spanner.AsyncResultSet toAsyncResultSet(com.google.cloud.spanner.ResultSet, com.google.api.gax.core.ExecutorProvider) + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSetImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSetImpl.java index 1cb768ea85a..fd172e96f9a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSetImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSetImpl.java @@ -25,6 +25,8 @@ import com.google.cloud.spanner.AbstractReadContext.ListenableAsyncResultSet; import com.google.common.base.Function; import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -88,8 +90,8 @@ private State(boolean shouldStop) { private final BlockingDeque buffer; private Struct currentRow; - /** The underlying synchronous {@link ResultSet} that is producing the rows. */ - private final ResultSet delegateResultSet; + /** Supplies the underlying synchronous {@link ResultSet} that will be producing the rows. */ + private final Supplier delegateResultSet; /** * Any exception that occurs while executing the query and iterating over the result set will be @@ -144,6 +146,11 @@ private State(boolean shouldStop) { private volatile CountDownLatch consumingLatch = new CountDownLatch(0); AsyncResultSetImpl(ExecutorProvider executorProvider, ResultSet delegate, int bufferSize) { + this(executorProvider, Suppliers.ofInstance(Preconditions.checkNotNull(delegate)), bufferSize); + } + + AsyncResultSetImpl( + ExecutorProvider executorProvider, Supplier delegate, int bufferSize) { super(delegate); this.executorProvider = Preconditions.checkNotNull(executorProvider); this.delegateResultSet = Preconditions.checkNotNull(delegate); @@ -165,7 +172,7 @@ public void close() { return; } if (state == State.INITIALIZED || state == State.SYNC) { - delegateResultSet.close(); + delegateResultSet.get().close(); } this.closed = true; } @@ -228,7 +235,7 @@ public CursorState tryNext() throws SpannerException { private void closeDelegateResultSet() { try { - delegateResultSet.close(); + delegateResultSet.get().close(); } catch (Throwable t) { log.log(Level.FINE, "Ignoring error from closing delegate result set", t); } @@ -261,7 +268,7 @@ public void run() { // we'll keep the cancelled state. return; } - executionException = SpannerExceptionFactory.newSpannerException(e); + executionException = SpannerExceptionFactory.asSpannerException(e); cursorReturnedDoneOrException = true; } return; @@ -325,10 +332,10 @@ public Void call() throws Exception { boolean stop = false; boolean hasNext = false; try { - hasNext = delegateResultSet.next(); + hasNext = delegateResultSet.get().next(); } catch (Throwable e) { synchronized (monitor) { - executionException = SpannerExceptionFactory.newSpannerException(e); + executionException = SpannerExceptionFactory.asSpannerException(e); } } try { @@ -357,13 +364,13 @@ public Void call() throws Exception { } } if (!stop) { - buffer.put(delegateResultSet.getCurrentRowAsStruct()); + buffer.put(delegateResultSet.get().getCurrentRowAsStruct()); startCallbackIfNecessary(); - hasNext = delegateResultSet.next(); + hasNext = delegateResultSet.get().next(); } } catch (Throwable e) { synchronized (monitor) { - executionException = SpannerExceptionFactory.newSpannerException(e); + executionException = SpannerExceptionFactory.asSpannerException(e); stop = true; } } @@ -544,9 +551,9 @@ public List toList(Function transformer) throws SpannerE try { return future.get(); } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause()); + throw SpannerExceptionFactory.asSpannerException(e.getCause()); } catch (Throwable e) { - throw SpannerExceptionFactory.newSpannerException(e); + throw SpannerExceptionFactory.asSpannerException(e); } } @@ -558,14 +565,14 @@ public boolean next() throws SpannerException { "Cannot call next() on a result set with a callback."); this.state = State.SYNC; } - boolean res = delegateResultSet.next(); - currentRow = res ? delegateResultSet.getCurrentRowAsStruct() : null; + boolean res = delegateResultSet.get().next(); + currentRow = res ? delegateResultSet.get().getCurrentRowAsStruct() : null; return res; } @Override public ResultSetStats getStats() { - return delegateResultSet.getStats(); + return delegateResultSet.get().getStats(); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ErrorCode.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ErrorCode.java index a9df5ab59b9..9896cc8aec9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ErrorCode.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ErrorCode.java @@ -89,7 +89,7 @@ static ErrorCode valueOf(String name, ErrorCode defaultValue) { /** * Returns the error code corresponding to a gRPC status, or {@code UNKNOWN} if not recognized. */ - static ErrorCode fromGrpcStatus(Status status) { + public static ErrorCode fromGrpcStatus(Status status) { ErrorCode code = errorByRpcCode.get(status.getCode().value()); return code == null ? UNKNOWN : code; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index ee9e715a251..5ec54960e6a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -16,14 +16,17 @@ package com.google.cloud.spanner; +import com.google.api.core.ApiFuture; import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.core.InstantiatingExecutorProvider; import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Type.Code; import com.google.cloud.spanner.Type.StructField; import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.spanner.v1.ResultSetStats; @@ -65,8 +68,41 @@ public static AsyncResultSet toAsyncResultSet(ResultSet delegate) { * ExecutorProvider}. */ public static AsyncResultSet toAsyncResultSet( - ResultSet delegate, ExecutorProvider executorProvider) { - return new AsyncResultSetImpl(executorProvider, delegate, 100); + ResultSet delegate, ExecutorProvider executorProvider, QueryOption... options) { + Options readOptions = Options.fromQueryOptions(options); + final int bufferRows = + readOptions.hasBufferRows() + ? readOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AsyncResultSetImpl(executorProvider, delegate, bufferRows); + } + + /** + * Converts the {@link ResultSet} that will be returned by the given {@link ApiFuture} to an + * {@link AsyncResultSet} using the given {@link ExecutorProvider}. + */ + public static AsyncResultSet toAsyncResultSet( + ApiFuture delegate, ExecutorProvider executorProvider, QueryOption... options) { + Options readOptions = Options.fromQueryOptions(options); + final int bufferRows = + readOptions.hasBufferRows() + ? readOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AsyncResultSetImpl( + executorProvider, new FutureResultSetSupplier(delegate), bufferRows); + } + + private static class FutureResultSetSupplier implements Supplier { + final ApiFuture delegate; + + FutureResultSetSupplier(ApiFuture delegate) { + this.delegate = Preconditions.checkNotNull(delegate); + } + + @Override + public ResultSet get() { + return SpannerApiFutures.get(delegate); + } } private static class PrePopulatedResultSet implements ResultSet { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java new file mode 100644 index 00000000000..39afc1b81a4 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.common.base.Preconditions; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; + +public class SpannerApiFutures { + public static T get(ApiFuture future) throws SpannerException { + return getOrNull(Preconditions.checkNotNull(future)); + } + + public static T getOrNull(ApiFuture future) throws SpannerException { + try { + return future == null ? null : future.get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof SpannerException) { + throw (SpannerException) e.getCause(); + } + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (CancellationException e) { + throw SpannerExceptionFactory.newSpannerExceptionForCancellation(null, e); + } + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java index 3fa756875b9..774aaf472e5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java @@ -83,6 +83,18 @@ public static SpannerException propagateTimeout(TimeoutException e) { ErrorCode.DEADLINE_EXCEEDED, "Operation did not complete in the given time", e); } + /** + * Converts the given {@link Throwable} to a {@link SpannerException}. If t is + * already a (subclass of a) {@link SpannerException}, t is returned unaltered. + * Otherwise, a new {@link SpannerException} is created with t as its cause. + */ + public static SpannerException asSpannerException(Throwable t) { + if (t instanceof SpannerException) { + return (SpannerException) t; + } + return newSpannerException(t); + } + /** * Creates a new exception based on {@code cause}. * @@ -126,6 +138,20 @@ public static SpannerBatchUpdateException newSpannerBatchUpdateException( databaseError); } + /** + * Constructs a new {@link AbortedDueToConcurrentModificationException} that can be re-thrown for + * a transaction that had already been aborted, but that the client application tried to use for + * additional statements. + */ + public static AbortedDueToConcurrentModificationException + newAbortedDueToConcurrentModificationException( + AbortedDueToConcurrentModificationException cause) { + return new AbortedDueToConcurrentModificationException( + DoNotConstructDirectly.ALLOWED, + "This transaction has already been aborted and could not be retried due to a concurrent modification. Rollback this transaction to start a new one.", + cause); + } + /** * Creates a new exception based on {@code cause}. If {@code cause} indicates cancellation, {@code * context} will be inspected to establish the type of cancellation. diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index dc6cb56f309..ab4a80b340f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -150,7 +150,7 @@ public void removeListener(Runnable listener) { @GuardedBy("lock") private long retryDelayInMillis = -1L; - private ByteString transactionId; + private volatile ByteString transactionId; private Timestamp commitTimestamp; private TransactionContextImpl(Builder builder) { @@ -238,12 +238,17 @@ void commit() { try { commitTimestamp = commitAsync().get(); } catch (InterruptedException e) { + if (commitFuture != null) { + commitFuture.cancel(true); + } throw SpannerExceptionFactory.propagateInterrupt(e); } catch (ExecutionException e) { throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()); } } + volatile ApiFuture commitFuture; + ApiFuture commitAsync() { final SettableApiFuture res = SettableApiFuture.create(); final SettableApiFuture latch; @@ -273,8 +278,7 @@ public void run() { span.addAnnotation("Starting Commit"); final Span opSpan = tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); - final ApiFuture commitFuture = - rpc.commitAsync(commitRequest, session.getOptions()); + commitFuture = rpc.commitAsync(commitRequest, session.getOptions()); commitFuture.addListener( tracer.withSpan( opSpan, diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java index 3fcffce0ac6..9ba86b3ec52 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java @@ -16,12 +16,27 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.grpc.GrpcCallContext; +import com.google.api.gax.longrunning.OperationFuture; +import com.google.api.gax.rpc.ApiCallContext; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.connection.StatementExecutor.StatementTimeout; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.Context; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; @@ -30,6 +45,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; /** Base for all {@link Connection}-based transactions and batches. */ @@ -37,12 +53,27 @@ abstract class AbstractBaseUnitOfWork implements UnitOfWork { private final StatementExecutor statementExecutor; private final StatementTimeout statementTimeout; + /** Class for keeping track of the stacktrace of the caller of an async statement. */ + static final class SpannerAsyncExecutionException extends RuntimeException { + final Statement statement; + + SpannerAsyncExecutionException(Statement statement) { + this.statement = statement; + } + + public String getMessage() { + // We only include the SQL of the statement and not the parameter values to prevent + // potentially sensitive data to escape into an error message. + return String.format("Execution failed for statement: %s", statement.getSql()); + } + } + /** * The {@link Future} that monitors the result of the statement currently being executed for this * unit of work. */ @GuardedBy("this") - private Future currentlyRunningStatementFuture = null; + private volatile Future currentlyRunningStatementFuture = null; enum InterceptorsUsage { INVOKE_INTERCEPTORS, @@ -100,34 +131,38 @@ public void cancel() { } } - T asyncExecuteStatement(ParsedStatement statement, Callable callable) { - return asyncExecuteStatement(statement, callable, InterceptorsUsage.INVOKE_INTERCEPTORS); + ApiFuture executeStatementAsync( + ParsedStatement statement, + Callable callable, + @Nullable MethodDescriptor applyStatementTimeoutToMethod) { + return executeStatementAsync( + statement, + callable, + InterceptorsUsage.INVOKE_INTERCEPTORS, + applyStatementTimeoutToMethod == null + ? Collections.>emptySet() + : ImmutableList.>of(applyStatementTimeoutToMethod)); } - T asyncExecuteStatement( - ParsedStatement statement, Callable callable, InterceptorsUsage interceptorUsage) { - Preconditions.checkNotNull(statement); - Preconditions.checkNotNull(callable); + ApiFuture executeStatementAsync( + ParsedStatement statement, + Callable callable, + Collection> applyStatementTimeoutToMethods) { + return executeStatementAsync( + statement, callable, InterceptorsUsage.INVOKE_INTERCEPTORS, applyStatementTimeoutToMethods); + } - if (interceptorUsage == InterceptorsUsage.INVOKE_INTERCEPTORS) { - statementExecutor.invokeInterceptors( - statement, StatementExecutionStep.EXECUTE_STATEMENT, this); - } - Future future = statementExecutor.submit(callable); - synchronized (this) { - this.currentlyRunningStatementFuture = future; - } - T res; + ResponseT getWithStatementTimeout( + OperationFuture operation, ParsedStatement statement) { + ResponseT res; try { if (statementTimeout.hasTimeout()) { TimeUnit unit = statementTimeout.getAppropriateTimeUnit(); - res = future.get(statementTimeout.getTimeoutValue(unit), unit); + res = operation.get(statementTimeout.getTimeoutValue(unit), unit); } else { - res = future.get(); + res = operation.get(); } } catch (TimeoutException e) { - // statement timed out, cancel the execution - future.cancel(true); throw SpannerExceptionFactory.newSpannerException( ErrorCode.DEADLINE_EXCEEDED, "Statement execution timeout occurred for " + statement.getSqlWithoutComments(), @@ -143,7 +178,7 @@ T asyncExecuteStatement( cause = cause.getCause(); } throw SpannerExceptionFactory.newSpannerException( - ErrorCode.UNKNOWN, + ErrorCode.fromGrpcStatus(Status.fromThrowable(e)), "Statement execution failed for " + statement.getSqlWithoutComments(), e); } catch (InterruptedException e) { @@ -152,11 +187,70 @@ T asyncExecuteStatement( } catch (CancellationException e) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.CANCELLED, "Statement execution was cancelled", e); - } finally { - synchronized (this) { - this.currentlyRunningStatementFuture = null; - } } return res; } + + ApiFuture executeStatementAsync( + ParsedStatement statement, + Callable callable, + InterceptorsUsage interceptorUsage, + final Collection> applyStatementTimeoutToMethods) { + Preconditions.checkNotNull(statement); + Preconditions.checkNotNull(callable); + + if (interceptorUsage == InterceptorsUsage.INVOKE_INTERCEPTORS) { + statementExecutor.invokeInterceptors( + statement, StatementExecutionStep.EXECUTE_STATEMENT, this); + } + Context context = Context.current(); + if (statementTimeout.hasTimeout() && !applyStatementTimeoutToMethods.isEmpty()) { + context = + context.withValue( + SpannerOptions.CALL_CONTEXT_CONFIGURATOR_KEY, + new SpannerOptions.CallContextConfigurator() { + @Override + public ApiCallContext configure( + ApiCallContext context, ReqT request, MethodDescriptor method) { + if (statementTimeout.hasTimeout() + && applyStatementTimeoutToMethods.contains(method)) { + return GrpcCallContext.createDefault() + .withTimeout(statementTimeout.asDuration()); + } + return null; + } + }); + } + ApiFuture f = statementExecutor.submit(context.wrap(callable)); + final SpannerAsyncExecutionException caller = + new SpannerAsyncExecutionException(statement.getStatement()); + final ApiFuture future = + ApiFutures.catching( + f, + Throwable.class, + new ApiFunction() { + @Override + public T apply(Throwable input) { + input.addSuppressed(caller); + throw SpannerExceptionFactory.asSpannerException(input); + } + }, + MoreExecutors.directExecutor()); + synchronized (this) { + this.currentlyRunningStatementFuture = future; + } + future.addListener( + new Runnable() { + @Override + public void run() { + synchronized (this) { + if (currentlyRunningStatementFuture == future) { + currentlyRunningStatementFuture = null; + } + } + } + }, + MoreExecutors.directExecutor()); + return future; + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java index cb8cf3bc557..33cef1fedb0 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.ReadContext; @@ -24,6 +25,7 @@ import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.common.base.Preconditions; +import com.google.spanner.v1.SpannerGrpc; import java.util.concurrent.Callable; /** @@ -46,6 +48,8 @@ public boolean isActive() { return getState().isActive(); } + abstract void checkAborted(); + /** * Check that the current transaction actually has a valid underlying transaction. If not, the * method will throw a {@link SpannerException}. @@ -55,22 +59,23 @@ public boolean isActive() { /** Returns the {@link ReadContext} that can be used for queries on this transaction. */ abstract ReadContext getReadContext(); - @Override - public ResultSet executeQuery( + public ApiFuture executeQueryAsync( final ParsedStatement statement, final AnalyzeMode analyzeMode, final QueryOption... options) { Preconditions.checkArgument(statement.isQuery(), "Statement is not a query"); checkValidTransaction(); - return asyncExecuteStatement( + return executeStatementAsync( statement, new Callable() { @Override public ResultSet call() throws Exception { + checkAborted(); return DirectExecuteResultSet.ofResultSet( internalExecuteQuery(statement, analyzeMode, options)); } - }); + }, + SpannerGrpc.getExecuteStreamingSqlMethod()); } ResultSet internalExecuteQuery( @@ -83,7 +88,7 @@ ResultSet internalExecuteQuery( } @Override - public long[] runBatch() { + public ApiFuture runBatchAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for transactions"); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResult.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResult.java new file mode 100644 index 00000000000..fef96ab456c --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResult.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import com.google.api.core.ApiFuture; +import com.google.api.core.InternalApi; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.connection.StatementResult.ResultType; + +@InternalApi +public interface AsyncStatementResult extends StatementResult { + /** + * Returns the {@link AsyncResultSet} held by this result. May only be called if the type of this + * result is {@link ResultType#RESULT_SET}. + * + * @return the {@link AsyncResultSet} held by this result. + */ + AsyncResultSet getResultSetAsync(); + + /** + * Returns the update count held by this result. May only be called if the type of this result is + * {@link ResultType#UPDATE_COUNT}. + * + * @return the update count held by this result. + */ + ApiFuture getUpdateCountAsync(); + + /** + * Returns a future that tracks the progress of a statement that returns no result. This could be + * a DDL statement or a client side statement that does not return a result. + */ + ApiFuture getNoResultAsync(); +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResultImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResultImpl.java new file mode 100644 index 00000000000..7d0b0fc3b5e --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AsyncStatementResultImpl.java @@ -0,0 +1,130 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.SpannerApiFutures.get; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.core.ExecutorProvider; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.ResultSets; +import com.google.common.base.Preconditions; + +class AsyncStatementResultImpl implements AsyncStatementResult { + + static AsyncStatementResult of(AsyncResultSet resultSet) { + return new AsyncStatementResultImpl(Preconditions.checkNotNull(resultSet), null); + } + + static AsyncStatementResult of(ApiFuture updateCount) { + return new AsyncStatementResultImpl(Preconditions.checkNotNull(updateCount)); + } + + static AsyncStatementResult of( + StatementResult clientSideStatementResult, ExecutorProvider executorProvider) { + Preconditions.checkNotNull(clientSideStatementResult.getClientSideStatementType()); + Preconditions.checkNotNull(executorProvider); + if (clientSideStatementResult.getResultType() == ResultType.RESULT_SET) { + return new AsyncStatementResultImpl( + ResultSets.toAsyncResultSet(clientSideStatementResult.getResultSet(), executorProvider), + clientSideStatementResult.getClientSideStatementType()); + } else { + return new AsyncStatementResultImpl( + clientSideStatementResult.getClientSideStatementType(), + ApiFutures.immediateFuture(null)); + } + } + + static AsyncStatementResult noResult(ApiFuture result) { + return new AsyncStatementResultImpl(null, Preconditions.checkNotNull(result)); + } + + private final ResultType type; + private final ClientSideStatementType clientSideStatementType; + private final AsyncResultSet resultSet; + private final ApiFuture updateCount; + private final ApiFuture noResult; + + private AsyncStatementResultImpl( + AsyncResultSet resultSet, ClientSideStatementType clientSideStatementType) { + this.type = ResultType.RESULT_SET; + this.clientSideStatementType = clientSideStatementType; + this.resultSet = resultSet; + this.updateCount = null; + this.noResult = null; + } + + private AsyncStatementResultImpl(ApiFuture updateCount) { + this.type = ResultType.UPDATE_COUNT; + this.clientSideStatementType = null; + this.resultSet = null; + this.updateCount = updateCount; + this.noResult = null; + } + + private AsyncStatementResultImpl( + ClientSideStatementType clientSideStatementType, ApiFuture result) { + this.type = ResultType.NO_RESULT; + this.clientSideStatementType = clientSideStatementType; + this.resultSet = null; + this.updateCount = null; + this.noResult = result; + } + + @Override + public ResultType getResultType() { + return type; + } + + @Override + public ClientSideStatementType getClientSideStatementType() { + return clientSideStatementType; + } + + @Override + public ResultSet getResultSet() { + return getResultSetAsync(); + } + + @Override + public Long getUpdateCount() { + return get(getUpdateCountAsync()); + } + + @Override + public AsyncResultSet getResultSetAsync() { + ConnectionPreconditions.checkState( + resultSet != null, "This result does not contain a ResultSet"); + return resultSet; + } + + @Override + public ApiFuture getUpdateCountAsync() { + ConnectionPreconditions.checkState( + updateCount != null, "This result does not contain an update count"); + return updateCount; + } + + @Override + public ApiFuture getNoResultAsync() { + ConnectionPreconditions.checkState( + type == ResultType.NO_RESULT, "This result does not contain a 'no-result' result"); + return noResult; + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java index 649d6c51fdf..f2d1ba548e4 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java @@ -66,7 +66,7 @@ @VisibleForTesting class ChecksumResultSet extends ReplaceableForwardingResultSet implements RetriableStatement { private final ReadWriteTransaction transaction; - private long numberOfNextCalls; + private volatile long numberOfNextCalls; private final ParsedStatement statement; private final AnalyzeMode analyzeMode; private final QueryOption[] options; @@ -98,7 +98,13 @@ public Boolean call() throws Exception { .getStatementExecutor() .invokeInterceptors( statement, StatementExecutionStep.CALL_NEXT_ON_RESULT_SET, transaction); - return ChecksumResultSet.super.next(); + boolean res = ChecksumResultSet.super.next(); + // Only update the checksum if there was another row to be consumed. + if (res) { + checksumCalculator.calculateNextChecksum(getCurrentRowAsStruct()); + } + numberOfNextCalls++; + return res; } } @@ -107,13 +113,7 @@ public Boolean call() throws Exception { @Override public boolean next() { // Call next() with retry. - boolean res = transaction.runWithRetry(nextCallable); - // Only update the checksum if there was another row to be consumed. - if (res) { - checksumCalculator.calculateNextChecksum(getCurrentRowAsStruct()); - } - numberOfNextCalls++; - return res; + return transaction.runWithRetry(nextCallable); } @VisibleForTesting diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java index 5247ce2c130..71b03e2e0b7 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java @@ -16,10 +16,12 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; import com.google.cloud.spanner.AbortedException; +import com.google.cloud.spanner.AsyncResultSet; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.Options.QueryOption; @@ -31,6 +33,7 @@ import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.connection.StatementResult.ResultType; import java.util.Iterator; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** @@ -41,6 +44,10 @@ * only exception is the {@link Connection#cancel()} method that may be called by any other thread * to stop the execution of the current statement on the connection. * + *

    All -Async methods on {@link Connection} are guaranteed to be executed in the order that they + * are issued on the {@link Connection}. Mixing synchronous and asynchronous method calls is also + * supported, and these are also guaranteed to be executed in the order that they are issued. + * *

    Connections accept a number of additional SQL statements for setting or changing the state of * a {@link Connection}. These statements can only be executed using the {@link * Connection#execute(Statement)} method: @@ -259,6 +266,25 @@ public interface Connection extends AutoCloseable { */ void beginTransaction(); + /** + * Begins a new transaction for this connection. This method is guaranteed to be non-blocking. The + * returned {@link ApiFuture} will be done when the transaction has been initialized. + * + *

      + *
    • Calling this method on a connection that has no transaction and that is + * not in autocommit mode, will register a new transaction that has not yet + * started on this connection + *
    • Calling this method on a connection that has no transaction and that is + * in autocommit mode, will register a new transaction that has not yet started on this + * connection, and temporarily turn off autocommit mode until the next commit/rollback + *
    • Calling this method on a connection that already has a transaction that has not yet + * started, will cause a {@link SpannerException} + *
    • Calling this method on a connection that already has a transaction that has started, will + * cause a {@link SpannerException} (no nested transactions) + *
    + */ + ApiFuture beginTransactionAsync(); + /** * Sets the transaction mode to use for current transaction. This method may only be called when * in a transaction, and before the transaction is actually started, i.e. before any statements @@ -450,6 +476,53 @@ public interface Connection extends AutoCloseable { */ void commit(); + /** + * Commits the current transaction of this connection. All mutations that have been buffered + * during the current transaction will be written to the database. + * + *

    This method is guaranteed to be non-blocking. The returned {@link ApiFuture} will be done + * when the transaction has committed or the commit has failed. + * + *

    Calling this method will always end the current transaction and start a new transaction when + * the next statement is executed, regardless whether this commit call succeeded or failed. If the + * next statement(s) rely on the results of the transaction that is being committed, it is + * recommended to check the status of this commit by inspecting the value of the returned {@link + * ApiFuture} before executing the next statement, to ensure that the commit actually succeeded. + * + *

    If the connection is in autocommit mode, and there is a temporary transaction active on this + * connection, calling this method will cause the connection to go back to autocommit mode after + * calling this method. + * + *

    This method will throw a {@link SpannerException} with code {@link + * ErrorCode#DEADLINE_EXCEEDED} if a statement timeout has been set on this connection, and the + * commit operation takes longer than this timeout. + * + *

      + *
    • Calling this method on a connection in autocommit mode and with no temporary transaction, + * will cause an exception + *
    • Calling this method while a DDL batch is active will cause an exception + *
    • Calling this method on a connection with a transaction that has not yet started, will end + * that transaction and any properties that might have been set on that transaction, and + * return the connection to its previous state. This means that if a transaction is created + * and set to read-only, and then committed before any statements have been executed, the + * read-only transaction is ended and any subsequent statements will be executed in a new + * transaction. If the connection is in read-write mode, the default for new transactions + * will be {@link TransactionMode#READ_WRITE_TRANSACTION}. Committing an empty transaction + * also does not generate a read timestamp or a commit timestamp, and calling one of the + * methods {@link Connection#getReadTimestamp()} or {@link Connection#getCommitTimestamp()} + * will cause an exception. + *
    • Calling this method on a connection with a {@link TransactionMode#READ_ONLY_TRANSACTION} + * transaction will end that transaction. If the connection is in read-write mode, any + * subsequent transaction will by default be a {@link + * TransactionMode#READ_WRITE_TRANSACTION} transaction, unless any following transaction is + * explicitly set to {@link TransactionMode#READ_ONLY_TRANSACTION} + *
    • Calling this method on a connection with a {@link TransactionMode#READ_WRITE_TRANSACTION} + * transaction will send all buffered mutations to the database, commit any DML statements + * that have been executed during this transaction and end the transaction. + *
    + */ + ApiFuture commitAsync(); + /** * Rollbacks the current transaction of this connection. All mutations or DDL statements that have * been buffered during the current transaction will be removed from the buffer. @@ -481,6 +554,40 @@ public interface Connection extends AutoCloseable { */ void rollback(); + /** + * Rollbacks the current transaction of this connection. All mutations or DDL statements that have + * been buffered during the current transaction will be removed from the buffer. + * + *

    This method is guaranteed to be non-blocking. The returned {@link ApiFuture} will be done + * when the transaction has been rolled back. + * + *

    If the connection is in autocommit mode, and there is a temporary transaction active on this + * connection, calling this method will cause the connection to go back to autocommit mode after + * calling this method. + * + *

      + *
    • Calling this method on a connection in autocommit mode and with no temporary transaction + * will cause an exception + *
    • Calling this method while a DDL batch is active will cause an exception + *
    • Calling this method on a connection with a transaction that has not yet started, will end + * that transaction and any properties that might have been set on that transaction, and + * return the connection to its previous state. This means that if a transaction is created + * and set to read-only, and then rolled back before any statements have been executed, the + * read-only transaction is ended and any subsequent statements will be executed in a new + * transaction. If the connection is in read-write mode, the default for new transactions + * will be {@link TransactionMode#READ_WRITE_TRANSACTION}. + *
    • Calling this method on a connection with a {@link TransactionMode#READ_ONLY_TRANSACTION} + * transaction will end that transaction. If the connection is in read-write mode, any + * subsequent transaction will by default be a {@link + * TransactionMode#READ_WRITE_TRANSACTION} transaction, unless any following transaction is + * explicitly set to {@link TransactionMode#READ_ONLY_TRANSACTION} + *
    • Calling this method on a connection with a {@link TransactionMode#READ_WRITE_TRANSACTION} + * transaction will clear all buffered mutations, rollback any DML statements that have been + * executed during this transaction and end the transaction. + *
    + */ + ApiFuture rollbackAsync(); + /** * @return true if this connection has a transaction (that has not necessarily * started). This method will only return false when the {@link Connection} is in autocommit @@ -572,11 +679,30 @@ public interface Connection extends AutoCloseable { *

    This method may only be called when a (possibly empty) batch is active. * * @return the update counts in case of a DML batch. Returns an array containing 1 for each - * successful statement and 0 for each failed statement or statement that was not executed DDL - * in case of a DDL batch. + * successful statement and 0 for each failed statement or statement that was not executed in + * case of a DDL batch. */ long[] runBatch(); + /** + * Sends all buffered DML or DDL statements of the current batch to the database, waits for these + * to be executed and ends the current batch. The method will throw an exception for the first + * statement that cannot be executed, or return successfully if all statements could be executed. + * If an exception is thrown for a statement in the batch, the preceding statements in the same + * batch may still have been applied to the database. + * + *

    This method is guaranteed to be non-blocking. The returned {@link ApiFuture} will be done + * when the batch has been successfully applied, or when one or more of the statements in the + * batch has failed and the further execution of the batch has been halted. + * + *

    This method may only be called when a (possibly empty) batch is active. + * + * @return an {@link ApiFuture} containing the update counts in case of a DML batch. The {@link + * ApiFuture} contains an array containing 1 for each successful statement and 0 for each + * failed statement or statement that was not executed in case of a DDL batch. + */ + ApiFuture runBatchAsync(); + /** * Clears all buffered statements in the current batch and ends the batch. * @@ -608,6 +734,30 @@ public interface Connection extends AutoCloseable { */ StatementResult execute(Statement statement); + /** + * Executes the given statement if allowed in the current {@link TransactionMode} and connection + * state asynchronously. The returned value depends on the type of statement: + * + *

      + *
    • Queries will return an {@link AsyncResultSet} + *
    • DML statements will return an {@link ApiFuture} with an update count that is done when + * the DML statement has been applied successfully, or that throws an {@link + * ExecutionException} if the DML statement failed. + *
    • DDL statements will return an {@link ApiFuture} containing a {@link Void} that is done + * when the DDL statement has been applied successfully, or that throws an {@link + * ExecutionException} if the DDL statement failed. + *
    • Connection and transaction statements (SET AUTOCOMMIT=TRUE|FALSE, SHOW AUTOCOMMIT, SET + * TRANSACTION READ ONLY, etc) will return either a {@link ResultSet} or {@link + * ResultType#NO_RESULT}, depending on the type of statement (SHOW or SET) + *
    + * + * This method is guaranteed to be non-blocking. + * + * @param statement The statement to execute + * @return the result of the statement + */ + AsyncStatementResult executeAsync(Statement statement); + /** * Executes the given statement as a query and returns the result as a {@link ResultSet}. This * method blocks and waits for a response from Spanner. If the statement does not contain a valid @@ -619,6 +769,31 @@ public interface Connection extends AutoCloseable { */ ResultSet executeQuery(Statement query, QueryOption... options); + /** + * Same as {@link #executeQuery(Statement, QueryOption...)}, but is guaranteed to be non-blocking + * and returns the query result as an {@link AsyncResultSet}. See {@link + * AsyncResultSet#setCallback(java.util.concurrent.Executor, + * com.google.cloud.spanner.AsyncResultSet.ReadyCallback)} for more information on how to consume + * the results of the query asynchronously. + */ + /** + * Executes the given statement asynchronously as a query and returns the result as an {@link + * AsyncResultSet}. This method is guaranteed to be non-blocking. If the statement does not + * contain a valid query, the method will throw a {@link SpannerException}. + * + *

    See {@link AsyncResultSet#setCallback(java.util.concurrent.Executor, + * com.google.cloud.spanner.AsyncResultSet.ReadyCallback)} for more information on how to consume + * the results of the query asynchronously. + * + *

    It is also possible to consume the returned {@link AsyncResultSet} in the same way as a + * normal {@link ResultSet}, i.e. in a while-loop calling {@link AsyncResultSet#next()}. + * + * @param query The query statement to execute + * @param options the options to configure the query + * @return an {@link AsyncResultSet} with the results of the query + */ + AsyncResultSet executeQueryAsync(Statement query, QueryOption... options); + /** * Analyzes a query and returns query plan and/or query execution statistics information. * @@ -655,6 +830,18 @@ public interface Connection extends AutoCloseable { */ long executeUpdate(Statement update); + /** + * Executes the given statement asynchronously as a DML statement. If the statement does not + * contain a valid DML statement, the method will throw a {@link SpannerException}. + * + *

    This method is guaranteed to be non-blocking. + * + * @param update The update statement to execute + * @return an {@link ApiFuture} containing the number of records that were + * inserted/updated/deleted by this statement + */ + ApiFuture executeUpdateAsync(Statement update); + /** * Executes a list of DML statements in a single request. The statements will be executed in order * and the semantics is the same as if each statement is executed by {@link @@ -677,6 +864,31 @@ public interface Connection extends AutoCloseable { */ long[] executeBatchUpdate(Iterable updates); + /** + * Executes a list of DML statements in a single request. The statements will be executed in order + * and the semantics is the same as if each statement is executed by {@link + * Connection#executeUpdate(Statement)} in a loop. This method returns an {@link ApiFuture} that + * contains an array of long integers, each representing the number of rows modified by each + * statement. + * + *

    This method is guaranteed to be non-blocking. + * + *

    If an individual statement fails, execution stops and a {@code SpannerBatchUpdateException} + * is returned, which includes the error and the number of rows affected by the statements that + * are run prior to the error. + * + *

    For example, if statements contains 3 statements, and the 2nd one is not a valid DML. This + * method throws a {@code SpannerBatchUpdateException} that contains the error message from the + * 2nd statement, and an array of length 1 that contains the number of rows modified by the 1st + * statement. The 3rd statement will not run. Executes the given statements as DML statements in + * one batch. If one of the statements does not contain a valid DML statement, the method will + * throw a {@link SpannerException}. + * + * @param updates The update statements that will be executed as one batch. + * @return an {@link ApiFuture} containing an array with the update counts per statement. + */ + ApiFuture executeBatchUpdateAsync(Iterable updates); + /** * Writes the specified mutation directly to the database and commits the change. The value is * readable after the successful completion of this method. Writing multiple mutations to a @@ -692,6 +904,23 @@ public interface Connection extends AutoCloseable { */ void write(Mutation mutation); + /** + * Writes the specified mutation directly to the database and commits the change. The value is + * readable after the successful completion of the returned {@link ApiFuture}. Writing multiple + * mutations to a database by calling this method multiple times mode is inefficient, as each call + * will need a round trip to the database. Instead, you should consider writing the mutations + * together by calling {@link Connection#writeAsync(Iterable)}. + * + *

    This method is guaranteed to be non-blocking. + * + *

    Calling this method is only allowed in autocommit mode. See {@link + * Connection#bufferedWrite(Iterable)} for writing mutations in transactions. + * + * @param mutation The {@link Mutation} to write to the database + * @throws SpannerException if the {@link Connection} is not in autocommit mode + */ + ApiFuture writeAsync(Mutation mutation); + /** * Writes the specified mutations directly to the database and commits the changes. The values are * readable after the successful completion of this method. @@ -704,6 +933,20 @@ public interface Connection extends AutoCloseable { */ void write(Iterable mutations); + /** + * Writes the specified mutations directly to the database and commits the changes. The values are + * readable after the successful completion of the returned {@link ApiFuture}. + * + *

    This method is guaranteed to be non-blocking. + * + *

    Calling this method is only allowed in autocommit mode. See {@link + * Connection#bufferedWrite(Iterable)} for writing mutations in transactions. + * + * @param mutations The {@link Mutation}s to write to the database + * @throws SpannerException if the {@link Connection} is not in autocommit mode + */ + ApiFuture writeAsync(Iterable mutations); + /** * Buffers the given mutation locally on the current transaction of this {@link Connection}. The * mutation will be written to the database at the next call to {@link Connection#commit()}. The diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java index ce24791859e..b49adbf1245 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java @@ -16,13 +16,19 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncResultSet; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.ResultSets; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; @@ -583,6 +589,11 @@ private void setDefaultTransactionOptions() { @Override public void beginTransaction() { + get(beginTransactionAsync()); + } + + @Override + public ApiFuture beginTransactionAsync() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState( !isBatchActive(), "This connection has an active batch and cannot begin a transaction"); @@ -596,17 +607,18 @@ public void beginTransaction() { if (isAutocommit()) { inTransaction = true; } + return ApiFutures.immediateFuture(null); } /** Internal interface for ending a transaction (commit/rollback). */ private static interface EndTransactionMethod { - public void end(UnitOfWork t); + public ApiFuture endAsync(UnitOfWork t); } private static final class Commit implements EndTransactionMethod { @Override - public void end(UnitOfWork t) { - t.commit(); + public ApiFuture endAsync(UnitOfWork t) { + return t.commitAsync(); } } @@ -614,14 +626,18 @@ public void end(UnitOfWork t) { @Override public void commit() { + get(commitAsync()); + } + + public ApiFuture commitAsync() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - endCurrentTransaction(commit); + return endCurrentTransactionAsync(commit); } private static final class Rollback implements EndTransactionMethod { @Override - public void end(UnitOfWork t) { - t.rollback(); + public ApiFuture endAsync(UnitOfWork t) { + return t.rollbackAsync(); } } @@ -629,18 +645,24 @@ public void end(UnitOfWork t) { @Override public void rollback() { + get(rollbackAsync()); + } + + public ApiFuture rollbackAsync() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - endCurrentTransaction(rollback); + return endCurrentTransactionAsync(rollback); } - private void endCurrentTransaction(EndTransactionMethod endTransactionMethod) { + private ApiFuture endCurrentTransactionAsync(EndTransactionMethod endTransactionMethod) { ConnectionPreconditions.checkState(!isBatchActive(), "This connection has an active batch"); ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction"); + ApiFuture res; try { if (isTransactionStarted()) { - endTransactionMethod.end(getCurrentUnitOfWorkOrStartNewUnitOfWork()); + res = endTransactionMethod.endAsync(getCurrentUnitOfWorkOrStartNewUnitOfWork()); } else { this.currentUnitOfWork = null; + res = ApiFutures.immediateFuture(null); } } finally { transactionBeginMarked = false; @@ -649,6 +671,7 @@ private void endCurrentTransaction(EndTransactionMethod endTransactionMethod) { } setDefaultTransactionOptions(); } + return res; } @Override @@ -664,9 +687,9 @@ public StatementResult execute(Statement statement) { case QUERY: return StatementResultImpl.of(internalExecuteQuery(parsedStatement, AnalyzeMode.NONE)); case UPDATE: - return StatementResultImpl.of(internalExecuteUpdate(parsedStatement)); + return StatementResultImpl.of(get(internalExecuteUpdateAsync(parsedStatement))); case DDL: - executeDdl(parsedStatement); + get(executeDdlAsync(parsedStatement)); return StatementResultImpl.noResult(); case UNKNOWN: default: @@ -676,11 +699,43 @@ public StatementResult execute(Statement statement) { "Unknown statement: " + parsedStatement.getSqlWithoutComments()); } + @Override + public AsyncStatementResult executeAsync(Statement statement) { + Preconditions.checkNotNull(statement); + ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); + ParsedStatement parsedStatement = parser.parse(statement, this.queryOptions); + switch (parsedStatement.getType()) { + case CLIENT_SIDE: + return AsyncStatementResultImpl.of( + parsedStatement + .getClientSideStatement() + .execute(connectionStatementExecutor, parsedStatement.getSqlWithoutComments()), + spanner.getAsyncExecutorProvider()); + case QUERY: + return AsyncStatementResultImpl.of( + internalExecuteQueryAsync(parsedStatement, AnalyzeMode.NONE)); + case UPDATE: + return AsyncStatementResultImpl.of(internalExecuteUpdateAsync(parsedStatement)); + case DDL: + return AsyncStatementResultImpl.noResult(executeDdlAsync(parsedStatement)); + case UNKNOWN: + default: + } + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Unknown statement: " + parsedStatement.getSqlWithoutComments()); + } + @Override public ResultSet executeQuery(Statement query, QueryOption... options) { return parseAndExecuteQuery(query, AnalyzeMode.NONE, options); } + @Override + public AsyncResultSet executeQueryAsync(Statement query, QueryOption... options) { + return parseAndExecuteQueryAsync(query, AnalyzeMode.NONE, options); + } + @Override public ResultSet analyzeQuery(Statement query, QueryAnalyzeMode queryMode) { Preconditions.checkNotNull(queryMode); @@ -717,6 +772,34 @@ private ResultSet parseAndExecuteQuery( "Statement is not a query: " + parsedStatement.getSqlWithoutComments()); } + private AsyncResultSet parseAndExecuteQueryAsync( + Statement query, AnalyzeMode analyzeMode, QueryOption... options) { + Preconditions.checkNotNull(query); + ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); + ParsedStatement parsedStatement = parser.parse(query, this.queryOptions); + if (parsedStatement.isQuery()) { + switch (parsedStatement.getType()) { + case CLIENT_SIDE: + return ResultSets.toAsyncResultSet( + parsedStatement + .getClientSideStatement() + .execute(connectionStatementExecutor, parsedStatement.getSqlWithoutComments()) + .getResultSet(), + spanner.getAsyncExecutorProvider(), + options); + case QUERY: + return internalExecuteQueryAsync(parsedStatement, analyzeMode, options); + case UPDATE: + case DDL: + case UNKNOWN: + default: + } + } + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Statement is not a query: " + parsedStatement.getSqlWithoutComments()); + } + @Override public long executeUpdate(Statement update) { Preconditions.checkNotNull(update); @@ -725,7 +808,27 @@ public long executeUpdate(Statement update) { if (parsedStatement.isUpdate()) { switch (parsedStatement.getType()) { case UPDATE: - return internalExecuteUpdate(parsedStatement); + return get(internalExecuteUpdateAsync(parsedStatement)); + case CLIENT_SIDE: + case QUERY: + case DDL: + case UNKNOWN: + default: + } + } + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Statement is not an update statement: " + parsedStatement.getSqlWithoutComments()); + } + + public ApiFuture executeUpdateAsync(Statement update) { + Preconditions.checkNotNull(update); + ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); + ParsedStatement parsedStatement = parser.parse(update); + if (parsedStatement.isUpdate()) { + switch (parsedStatement.getType()) { + case UPDATE: + return internalExecuteUpdateAsync(parsedStatement); case CLIENT_SIDE: case QUERY: case DDL: @@ -746,24 +849,48 @@ public long[] executeBatchUpdate(Iterable updates) { List parsedStatements = new LinkedList<>(); for (Statement update : updates) { ParsedStatement parsedStatement = parser.parse(update); - if (parsedStatement.isUpdate()) { - switch (parsedStatement.getType()) { - case UPDATE: - parsedStatements.add(parsedStatement); - break; - case CLIENT_SIDE: - case QUERY: - case DDL: - case UNKNOWN: - default: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "The batch update list contains a statement that is not an update statement: " - + parsedStatement.getSqlWithoutComments()); - } + switch (parsedStatement.getType()) { + case UPDATE: + parsedStatements.add(parsedStatement); + break; + case CLIENT_SIDE: + case QUERY: + case DDL: + case UNKNOWN: + default: + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "The batch update list contains a statement that is not an update statement: " + + parsedStatement.getSqlWithoutComments()); + } + } + return get(internalExecuteBatchUpdateAsync(parsedStatements)); + } + + @Override + public ApiFuture executeBatchUpdateAsync(Iterable updates) { + Preconditions.checkNotNull(updates); + ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); + // Check that there are only DML statements in the input. + List parsedStatements = new LinkedList<>(); + for (Statement update : updates) { + ParsedStatement parsedStatement = parser.parse(update); + switch (parsedStatement.getType()) { + case UPDATE: + parsedStatements.add(parsedStatement); + break; + case CLIENT_SIDE: + case QUERY: + case DDL: + case UNKNOWN: + default: + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "The batch update list contains a statement that is not an update statement: " + + parsedStatement.getSqlWithoutComments()); } } - return internalExecuteBatchUpdate(parsedStatements); + return internalExecuteBatchUpdateAsync(parsedStatements); } private ResultSet internalExecuteQuery( @@ -773,52 +900,32 @@ private ResultSet internalExecuteQuery( Preconditions.checkArgument( statement.getType() == StatementType.QUERY, "Statement must be a query"); UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(); - try { - return transaction.executeQuery(statement, analyzeMode, options); - } catch (SpannerException e) { - // In case of a timed out or cancelled query we need to replace the executor to ensure that we - // have an executor that is not busy executing a statement. Although we try to cancel the - // current statement, it is not guaranteed to actually stop the execution directly. - if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED - || e.getErrorCode() == ErrorCode.CANCELLED) { - this.statementExecutor.recreate(); - } - throw e; - } + return get(transaction.executeQueryAsync(statement, analyzeMode, options)); } - private long internalExecuteUpdate(final ParsedStatement update) { + private AsyncResultSet internalExecuteQueryAsync( + final ParsedStatement statement, + final AnalyzeMode analyzeMode, + final QueryOption... options) { + Preconditions.checkArgument( + statement.getType() == StatementType.QUERY, "Statement must be a query"); + UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(); + return ResultSets.toAsyncResultSet( + transaction.executeQueryAsync(statement, analyzeMode, options), + spanner.getAsyncExecutorProvider(), + options); + } + + private ApiFuture internalExecuteUpdateAsync(final ParsedStatement update) { Preconditions.checkArgument( update.getType() == StatementType.UPDATE, "Statement must be an update"); UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(); - try { - return transaction.executeUpdate(update); - } catch (SpannerException e) { - // In case of a timed out or cancelled query we need to replace the executor to ensure that we - // have an executor that is not busy executing a statement. Although we try to cancel the - // current statement, it is not guaranteed to actually stop the execution directly. - if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED - || e.getErrorCode() == ErrorCode.CANCELLED) { - this.statementExecutor.recreate(); - } - throw e; - } + return transaction.executeUpdateAsync(update); } - private long[] internalExecuteBatchUpdate(final List updates) { + private ApiFuture internalExecuteBatchUpdateAsync(List updates) { UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(); - try { - return transaction.executeBatchUpdate(updates); - } catch (SpannerException e) { - // In case of a timed out or cancelled query we need to replace the executor to ensure that we - // have an executor that is not busy executing a statement. Although we try to cancel the - // current statement, it is not guaranteed to actually stop the execution directly. - if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED - || e.getErrorCode() == ErrorCode.CANCELLED) { - this.statementExecutor.recreate(); - } - throw e; - } + return transaction.executeBatchUpdateAsync(updates); } /** @@ -898,32 +1005,36 @@ private void popUnitOfWorkFromTransactionStack() { this.currentUnitOfWork = transactionStack.pop(); } - private void executeDdl(ParsedStatement ddl) { - getCurrentUnitOfWorkOrStartNewUnitOfWork().executeDdl(ddl); + private ApiFuture executeDdlAsync(ParsedStatement ddl) { + return getCurrentUnitOfWorkOrStartNewUnitOfWork().executeDdlAsync(ddl); } @Override public void write(Mutation mutation) { - Preconditions.checkNotNull(mutation); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(isAutocommit(), ONLY_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutation); + get(writeAsync(Collections.singleton(Preconditions.checkNotNull(mutation)))); + } + + @Override + public ApiFuture writeAsync(Mutation mutation) { + return writeAsync(Collections.singleton(Preconditions.checkNotNull(mutation))); } @Override public void write(Iterable mutations) { + get(writeAsync(Preconditions.checkNotNull(mutations))); + } + + @Override + public ApiFuture writeAsync(Iterable mutations) { Preconditions.checkNotNull(mutations); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState(isAutocommit(), ONLY_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutations); + return getCurrentUnitOfWorkOrStartNewUnitOfWork().writeAsync(mutations); } @Override public void bufferedWrite(Mutation mutation) { - Preconditions.checkNotNull(mutation); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isAutocommit(), NOT_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutation); + bufferedWrite(Preconditions.checkNotNull(Collections.singleton(mutation))); } @Override @@ -931,7 +1042,7 @@ public void bufferedWrite(Iterable mutations) { Preconditions.checkNotNull(mutations); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState(!isAutocommit(), NOT_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutations); + get(getCurrentUnitOfWorkOrStartNewUnitOfWork().writeAsync(mutations)); } @Override @@ -973,13 +1084,18 @@ public void startBatchDml() { @Override public long[] runBatch() { + return get(runBatchAsync()); + } + + @Override + public ApiFuture runBatchAsync() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState(isBatchActive(), "This connection has no active batch"); try { if (this.currentUnitOfWork != null) { - return this.currentUnitOfWork.runBatch(); + return this.currentUnitOfWork.runBatchAsync(); } - return new long[0]; + return ApiFutures.immediateFuture(new long[0]); } finally { this.batchMode = BatchMode.NONE; setDefaultTransactionOptions(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 379459884c7..d2a341430ef 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -237,6 +237,15 @@ public static void closeSpanner() { SpannerPool.INSTANCE.checkAndCloseSpanners(); } + /** + * {@link SpannerOptionsConfigurator} can be used to add additional configuration for a {@link + * Spanner} instance. Intended for tests. + */ + @VisibleForTesting + interface SpannerOptionsConfigurator { + void configure(SpannerOptions.Builder options); + } + /** Builder for {@link ConnectionOptions} instances. */ public static class Builder { private String uri; @@ -246,6 +255,7 @@ public static class Builder { private SessionPoolOptions sessionPoolOptions; private List statementExecutionInterceptors = Collections.emptyList(); + private SpannerOptionsConfigurator configurator; private Builder() {} @@ -358,6 +368,12 @@ Builder setStatementExecutionInterceptors(List in return this; } + @VisibleForTesting + Builder setConfigurator(SpannerOptionsConfigurator configurator) { + this.configurator = Preconditions.checkNotNull(configurator); + return this; + } + @VisibleForTesting Builder setCredentials(Credentials credentials) { this.credentials = credentials; @@ -401,6 +417,7 @@ public static Builder newBuilder() { private final boolean readOnly; private final boolean retryAbortsInternally; private final List statementExecutionInterceptors; + private final SpannerOptionsConfigurator configurator; private ConnectionOptions(Builder builder) { Matcher matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri); @@ -473,6 +490,11 @@ private ConnectionOptions(Builder builder) { this.retryAbortsInternally = parseRetryAbortsInternally(this.uri); this.statementExecutionInterceptors = Collections.unmodifiableList(builder.statementExecutionInterceptors); + this.configurator = builder.configurator; + } + + SpannerOptionsConfigurator getConfigurator() { + return configurator; } @VisibleForTesting diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java index a80e93dfc08..7d4f18c4dbc 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.Timestamp; import com.google.cloud.spanner.DatabaseClient; @@ -31,15 +33,14 @@ import com.google.cloud.spanner.connection.StatementParser.StatementType; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.spanner.admin.database.v1.DatabaseAdminGrpc; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.v1.SpannerGrpc; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** * {@link UnitOfWork} that is used when a DDL batch is started. These batches only accept DDL @@ -111,8 +112,7 @@ public boolean isReadOnly() { return false; } - @Override - public ResultSet executeQuery( + public ApiFuture executeQueryAsync( final ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { if (options != null) { for (int i = 0; i < options.length; i++) { @@ -136,7 +136,8 @@ public ResultSet call() throws Exception { dbClient.singleUse().executeQuery(statement.getStatement(), internalOptions)); } }; - return asyncExecuteStatement(statement, callable); + return executeStatementAsync( + statement, callable, SpannerGrpc.getExecuteStreamingSqlMethod()); } } } @@ -168,7 +169,7 @@ public Timestamp getCommitTimestampOrNull() { } @Override - public void executeDdl(ParsedStatement ddl) { + public ApiFuture executeDdlAsync(ParsedStatement ddl) { ConnectionPreconditions.checkState( state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be used for further statements"); @@ -178,28 +179,23 @@ public void executeDdl(ParsedStatement ddl) { + ddl.getSqlWithoutComments() + "\" is not a DDL-statement."); statements.add(ddl.getSqlWithoutComments()); + return ApiFutures.immediateFuture(null); } @Override - public long executeUpdate(ParsedStatement update) { + public ApiFuture executeUpdateAsync(ParsedStatement update) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Executing updates is not allowed for DDL batches."); } @Override - public long[] executeBatchUpdate(Iterable updates) { + public ApiFuture executeBatchUpdateAsync(Iterable updates) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Executing batch updates is not allowed for DDL batches."); } @Override - public void write(Mutation mutation) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DDL batches."); - } - - @Override - public void write(Iterable mutations) { + public ApiFuture writeAsync(Iterable mutations) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DDL batches."); } @@ -214,62 +210,50 @@ public void write(Iterable mutations) { StatementParser.INSTANCE.parse(Statement.of("RUN BATCH")); @Override - public long[] runBatch() { + public ApiFuture runBatchAsync() { ConnectionPreconditions.checkState( state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be ran"); - try { - if (!statements.isEmpty()) { - // create a statement that can be passed in to the execute method - Callable callable = - new Callable() { - @Override - public UpdateDatabaseDdlMetadata call() throws Exception { - OperationFuture operation = - ddlClient.executeDdl(statements); - try { - // Wait until the operation has finished. - operation.get(); - // Return metadata. - return operation.getMetadata().get(); - } catch (ExecutionException e) { - SpannerException spannerException = extractSpannerCause(e); - long[] updateCounts = extractUpdateCounts(operation.getMetadata().get()); - throw SpannerExceptionFactory.newSpannerBatchUpdateException( - spannerException == null - ? ErrorCode.UNKNOWN - : spannerException.getErrorCode(), - e.getMessage(), - updateCounts); - } catch (InterruptedException e) { - long[] updateCounts = extractUpdateCounts(operation.getMetadata().get()); - throw SpannerExceptionFactory.newSpannerBatchUpdateException( - ErrorCode.CANCELLED, e.getMessage(), updateCounts); - } - } - }; - asyncExecuteStatement(RUN_BATCH, callable); - } + if (statements.isEmpty()) { this.state = UnitOfWorkState.RAN; - long[] updateCounts = new long[statements.size()]; - Arrays.fill(updateCounts, 1L); - return updateCounts; - } catch (SpannerException e) { - this.state = UnitOfWorkState.RUN_FAILED; - throw e; + return ApiFutures.immediateFuture(new long[0]); } + // create a statement that can be passed in to the execute method + Callable callable = + new Callable() { + @Override + public long[] call() throws Exception { + try { + OperationFuture operation = + ddlClient.executeDdl(statements); + try { + // Wait until the operation has finished. + getWithStatementTimeout(operation, RUN_BATCH); + long[] updateCounts = new long[statements.size()]; + Arrays.fill(updateCounts, 1L); + state = UnitOfWorkState.RAN; + return updateCounts; + } catch (SpannerException e) { + long[] updateCounts = extractUpdateCounts(operation); + throw SpannerExceptionFactory.newSpannerBatchUpdateException( + e.getErrorCode(), e.getMessage(), updateCounts); + } + } catch (Throwable t) { + state = UnitOfWorkState.RUN_FAILED; + throw t; + } + } + }; + this.state = UnitOfWorkState.RUNNING; + return executeStatementAsync( + RUN_BATCH, callable, DatabaseAdminGrpc.getUpdateDatabaseDdlMethod()); } - private SpannerException extractSpannerCause(ExecutionException e) { - Throwable cause = e.getCause(); - Set causes = new HashSet<>(); - while (cause != null && !causes.contains(cause)) { - if (cause instanceof SpannerException) { - return (SpannerException) cause; - } - causes.add(cause); - cause = cause.getCause(); + long[] extractUpdateCounts(OperationFuture operation) { + try { + return extractUpdateCounts(operation.getMetadata().get()); + } catch (Throwable t) { + return new long[0]; } - return null; } @VisibleForTesting @@ -293,13 +277,13 @@ public void abortBatch() { } @Override - public void commit() { + public ApiFuture commitAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Commit is not allowed for DDL batches."); } @Override - public void rollback() { + public ApiFuture rollbackAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Rollback is not allowed for DDL batches."); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java index ff38338d623..b5b80e46cf9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java @@ -16,16 +16,20 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; import com.google.cloud.Timestamp; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.cloud.spanner.connection.StatementParser.StatementType; import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; @@ -87,7 +91,7 @@ public boolean isReadOnly() { } @Override - public ResultSet executeQuery( + public ApiFuture executeQueryAsync( ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Executing queries is not allowed for DML batches."); @@ -116,13 +120,13 @@ public Timestamp getCommitTimestampOrNull() { } @Override - public void executeDdl(ParsedStatement ddl) { + public ApiFuture executeDdlAsync(ParsedStatement ddl) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Executing DDL statements is not allowed for DML batches."); } @Override - public long executeUpdate(ParsedStatement update) { + public ApiFuture executeUpdateAsync(ParsedStatement update) { ConnectionPreconditions.checkState( state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be used for further statements"); @@ -132,44 +136,54 @@ public long executeUpdate(ParsedStatement update) { + update.getSqlWithoutComments() + "\" is not a DML-statement."); statements.add(update); - return -1L; + return ApiFutures.immediateFuture(-1L); } @Override - public long[] executeBatchUpdate(Iterable updates) { + public ApiFuture executeBatchUpdateAsync(Iterable updates) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Executing batch updates is not allowed for DML batches."); } @Override - public void write(Mutation mutation) { + public ApiFuture writeAsync(Iterable mutations) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DML batches."); } @Override - public void write(Iterable mutations) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DML batches."); - } - - @Override - public long[] runBatch() { + public ApiFuture runBatchAsync() { ConnectionPreconditions.checkState( state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be ran"); - try { - long[] res; - if (statements.isEmpty()) { - res = new long[0]; - } else { - res = transaction.executeBatchUpdate(statements); - } + if (statements.isEmpty()) { this.state = UnitOfWorkState.RAN; - return res; - } catch (SpannerException e) { - this.state = UnitOfWorkState.RUN_FAILED; - throw e; + return ApiFutures.immediateFuture(new long[0]); } + this.state = UnitOfWorkState.RUNNING; + // Use a SettableApiFuture to return the result, instead of directly returning the future that + // is returned by the executeBatchUpdateAsync method. This is needed because the state of the + // batch is set after the update has finished, and this happens in a listener. A listener is + // executed AFTER a Future is done, which means that a user could read the state of the Batch + // before it has been changed. + final SettableApiFuture res = SettableApiFuture.create(); + ApiFuture updateCounts = transaction.executeBatchUpdateAsync(statements); + ApiFutures.addCallback( + updateCounts, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + state = UnitOfWorkState.RUN_FAILED; + res.setException(t); + } + + @Override + public void onSuccess(long[] result) { + state = UnitOfWorkState.RAN; + res.set(result); + } + }, + MoreExecutors.directExecutor()); + return res; } @Override @@ -180,13 +194,13 @@ public void abortBatch() { } @Override - public void commit() { + public ApiFuture commitAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Commit is not allowed for DML batches."); } @Override - public void rollback() { + public ApiFuture rollbackAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Rollback is not allowed for DML batches."); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java index c9435886c04..09f3efc6d59 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.cloud.Timestamp; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.ErrorCode; @@ -83,6 +85,11 @@ public boolean isReadOnly() { return true; } + @Override + void checkAborted() { + // No-op for read-only transactions as they cannot abort. + } + @Override void checkValidTransaction() { if (transaction == null) { @@ -130,49 +137,45 @@ public Timestamp getCommitTimestampOrNull() { } @Override - public void executeDdl(ParsedStatement ddl) { + public ApiFuture executeDdlAsync(ParsedStatement ddl) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "DDL statements are not allowed for read-only transactions"); } @Override - public long executeUpdate(ParsedStatement update) { + public ApiFuture executeUpdateAsync(ParsedStatement update) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Update statements are not allowed for read-only transactions"); } @Override - public long[] executeBatchUpdate(Iterable updates) { + public ApiFuture executeBatchUpdateAsync(Iterable updates) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Batch updates are not allowed for read-only transactions."); } @Override - public void write(Mutation mutation) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Mutations are not allowed for read-only transactions"); - } - - @Override - public void write(Iterable mutations) { + public ApiFuture writeAsync(Iterable mutations) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Mutations are not allowed for read-only transactions"); } @Override - public void commit() { + public ApiFuture commitAsync() { if (this.transaction != null) { this.transaction.close(); } this.state = UnitOfWorkState.COMMITTED; + return ApiFutures.immediateFuture(null); } @Override - public void rollback() { + public ApiFuture rollbackAsync() { if (this.transaction != null) { this.transaction.close(); } this.state = UnitOfWorkState.ROLLED_BACK; + return ApiFutures.immediateFuture(null); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java index 7a0155cbfb8..0a8e322e796 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java @@ -16,8 +16,13 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; import com.google.cloud.spanner.AbortedException; @@ -35,6 +40,10 @@ import com.google.cloud.spanner.connection.TransactionRetryListener.RetryResult; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.spanner.v1.SpannerGrpc; +import io.grpc.MethodDescriptor; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -65,12 +74,15 @@ class ReadWriteTransaction extends AbstractMultiUseTransaction { private int transactionRetryAttempts; private int successfulRetries; private final List transactionRetryListeners; - private volatile TransactionContext txContext; + private volatile ApiFuture txContextFuture; + private volatile SettableApiFuture commitTimestampFuture; private volatile UnitOfWorkState state = UnitOfWorkState.STARTED; + private volatile AbortedException abortedException; private boolean timedOutOrCancelled = false; private final List statements = new ArrayList<>(); private final List mutations = new ArrayList<>(); private Timestamp transactionStarted; + final Object abortedLock = new Object(); static class Builder extends AbstractMultiUseTransaction.Builder { private DatabaseClient dbClient; @@ -154,36 +166,80 @@ public boolean isReadOnly() { return false; } + private static final ParsedStatement BEGIN_STATEMENT = + StatementParser.INSTANCE.parse(Statement.of("BEGIN")); + @Override void checkValidTransaction() { + checkValidState(); + if (txContextFuture == null) { + transactionStarted = Timestamp.now(); + txContextFuture = + executeStatementAsync( + BEGIN_STATEMENT, + new Callable() { + @Override + public TransactionContext call() throws Exception { + return txManager.begin(); + } + }, + SpannerGrpc.getBeginTransactionMethod()); + } + } + + private void checkValidState() { ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, + this.state == UnitOfWorkState.STARTED || this.state == UnitOfWorkState.ABORTED, "This transaction has status " - + state.name() + + this.state.name() + ", only " + UnitOfWorkState.STARTED + + "or " + + UnitOfWorkState.ABORTED + " is allowed."); ConnectionPreconditions.checkState( !timedOutOrCancelled, "The last statement of this transaction timed out or was cancelled. " + "The transaction is no longer usable. " + "Rollback the transaction and start a new one."); - if (txManager.getState() == null) { - transactionStarted = Timestamp.now(); - txContext = txManager.begin(); - } - if (txManager.getState() - != com.google.cloud.spanner.TransactionManager.TransactionState.STARTED) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - String.format("Invalid transaction state: %s", txManager.getState())); + } + + @Override + public boolean isActive() { + // Consider ABORTED an active state, as it is something that is automatically set if the + // transaction is aborted by the backend. That means that we should not automatically create a + // new transaction for the following statement after a transaction has aborted, and instead we + // should wait until the application has rolled back the current transaction. + // + // Othwerwise the following list of statements could show unexpected behavior: + + // connection.executeUpdateAsync("UPDATE FOO SET BAR=1 ..."); + // connection.executeUpdateAsync("UPDATE BAR SET FOO=2 ..."); + // connection.commitAsync(); + // + // If the first update statement fails with an aborted exception, the second update statement + // should not be executed in a new transaction, but should also abort. + return getState().isActive() || state == UnitOfWorkState.ABORTED; + } + + void checkAborted() { + if (this.state == UnitOfWorkState.ABORTED && this.abortedException != null) { + if (this.abortedException instanceof AbortedDueToConcurrentModificationException) { + throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException( + (AbortedDueToConcurrentModificationException) this.abortedException); + } else { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.ABORTED, + "This transaction has already been aborted. Rollback this transaction to start a new one.", + this.abortedException); + } } } @Override TransactionContext getReadContext() { - ConnectionPreconditions.checkState(txContext != null, "Missing transaction context"); - return txContext; + ConnectionPreconditions.checkState(txContextFuture != null, "Missing transaction context"); + return get(txContextFuture); } @Override @@ -199,23 +255,22 @@ public Timestamp getReadTimestampOrNull() { } private boolean hasCommitTimestamp() { - return txManager.getState() - == com.google.cloud.spanner.TransactionManager.TransactionState.COMMITTED; + return commitTimestampFuture != null; } @Override public Timestamp getCommitTimestamp() { ConnectionPreconditions.checkState(hasCommitTimestamp(), "This transaction has not committed."); - return txManager.getCommitTimestamp(); + return get(commitTimestampFuture); } @Override public Timestamp getCommitTimestampOrNull() { - return hasCommitTimestamp() ? txManager.getCommitTimestamp() : null; + return hasCommitTimestamp() ? get(commitTimestampFuture) : null; } @Override - public void executeDdl(ParsedStatement ddl) { + public ApiFuture executeDdlAsync(ParsedStatement ddl) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "DDL-statements are not allowed inside a read/write transaction."); @@ -229,108 +284,138 @@ private void handlePossibleInvalidatingException(SpannerException e) { } @Override - public ResultSet executeQuery( + public ApiFuture executeQueryAsync( final ParsedStatement statement, final AnalyzeMode analyzeMode, final QueryOption... options) { Preconditions.checkArgument(statement.isQuery(), "Statement is not a query"); checkValidTransaction(); - try { - if (retryAbortsInternally) { - return asyncExecuteStatement( - statement, - new Callable() { - @Override - public ResultSet call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public ResultSet call() throws Exception { - try { - getStatementExecutor() - .invokeInterceptors( - statement, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - ResultSet delegate = - DirectExecuteResultSet.ofResultSet( - internalExecuteQuery(statement, analyzeMode, options)); - return createAndAddRetryResultSet( - delegate, statement, analyzeMode, options); - } catch (AbortedException e) { - throw e; - } catch (SpannerException e) { - createAndAddFailedQuery(e, statement, analyzeMode, options); - throw e; + + ApiFuture res; + if (retryAbortsInternally) { + res = + executeStatementAsync( + statement, + new Callable() { + @Override + public ResultSet call() throws Exception { + return runWithRetry( + new Callable() { + @Override + public ResultSet call() throws Exception { + try { + getStatementExecutor() + .invokeInterceptors( + statement, + StatementExecutionStep.EXECUTE_STATEMENT, + ReadWriteTransaction.this); + ResultSet delegate = + DirectExecuteResultSet.ofResultSet( + internalExecuteQuery(statement, analyzeMode, options)); + return createAndAddRetryResultSet( + delegate, statement, analyzeMode, options); + } catch (AbortedException e) { + throw e; + } catch (SpannerException e) { + createAndAddFailedQuery(e, statement, analyzeMode, options); + throw e; + } } - } - }); - } - }, - InterceptorsUsage - .IGNORE_INTERCEPTORS); // ignore interceptors here as they are invoked in the - // Callable. - } else { - return super.executeQuery(statement, analyzeMode, options); - } - } catch (SpannerException e) { - handlePossibleInvalidatingException(e); - throw e; + }); + } + }, + // ignore interceptors here as they are invoked in the Callable. + InterceptorsUsage.IGNORE_INTERCEPTORS, + ImmutableList.>of(SpannerGrpc.getExecuteStreamingSqlMethod())); + } else { + res = super.executeQueryAsync(statement, analyzeMode, options); } + + ApiFutures.addCallback( + res, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + if (t instanceof SpannerException) { + handlePossibleInvalidatingException((SpannerException) t); + } + } + + @Override + public void onSuccess(ResultSet result) {} + }, + MoreExecutors.directExecutor()); + return res; } @Override - public long executeUpdate(final ParsedStatement update) { + public ApiFuture executeUpdateAsync(final ParsedStatement update) { Preconditions.checkNotNull(update); Preconditions.checkArgument(update.isUpdate(), "The statement is not an update statement"); checkValidTransaction(); - try { - if (retryAbortsInternally) { - return asyncExecuteStatement( - update, - new Callable() { - @Override - public Long call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public Long call() throws Exception { - try { - getStatementExecutor() - .invokeInterceptors( - update, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - long updateCount = txContext.executeUpdate(update.getStatement()); - createAndAddRetriableUpdate(update, updateCount); - return updateCount; - } catch (AbortedException e) { - throw e; - } catch (SpannerException e) { - createAndAddFailedUpdate(e, update); - throw e; + ApiFuture res; + if (retryAbortsInternally) { + res = + executeStatementAsync( + update, + new Callable() { + @Override + public Long call() throws Exception { + return runWithRetry( + new Callable() { + @Override + public Long call() throws Exception { + try { + getStatementExecutor() + .invokeInterceptors( + update, + StatementExecutionStep.EXECUTE_STATEMENT, + ReadWriteTransaction.this); + long updateCount = + get(txContextFuture).executeUpdate(update.getStatement()); + createAndAddRetriableUpdate(update, updateCount); + return updateCount; + } catch (AbortedException e) { + throw e; + } catch (SpannerException e) { + createAndAddFailedUpdate(e, update); + throw e; + } } - } - }); - } - }, - InterceptorsUsage - .IGNORE_INTERCEPTORS); // ignore interceptors here as they are invoked in the - // Callable. - } else { - return asyncExecuteStatement( - update, - new Callable() { - @Override - public Long call() throws Exception { - return txContext.executeUpdate(update.getStatement()); - } - }); - } - } catch (SpannerException e) { - handlePossibleInvalidatingException(e); - throw e; + }); + } + }, + // ignore interceptors here as they are invoked in the Callable. + InterceptorsUsage.IGNORE_INTERCEPTORS, + ImmutableList.>of(SpannerGrpc.getExecuteSqlMethod())); + } else { + res = + executeStatementAsync( + update, + new Callable() { + @Override + public Long call() throws Exception { + checkAborted(); + return get(txContextFuture).executeUpdate(update.getStatement()); + } + }, + SpannerGrpc.getExecuteSqlMethod()); } + ApiFutures.addCallback( + res, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + if (t instanceof SpannerException) { + handlePossibleInvalidatingException((SpannerException) t); + } + } + + @Override + public void onSuccess(Long result) {} + }, + MoreExecutors.directExecutor()); + return res; } /** @@ -348,7 +433,7 @@ public Long call() throws Exception { StatementParser.INSTANCE.parse(Statement.of("RUN BATCH")); @Override - public long[] executeBatchUpdate(final Iterable updates) { + public ApiFuture executeBatchUpdateAsync(Iterable updates) { Preconditions.checkNotNull(updates); final List updateStatements = new LinkedList<>(); for (ParsedStatement update : updates) { @@ -358,69 +443,81 @@ public long[] executeBatchUpdate(final Iterable updates) { updateStatements.add(update.getStatement()); } checkValidTransaction(); - try { - if (retryAbortsInternally) { - return asyncExecuteStatement( - EXECUTE_BATCH_UPDATE_STATEMENT, - new Callable() { - @Override - public long[] call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public long[] call() throws Exception { - try { - getStatementExecutor() - .invokeInterceptors( - EXECUTE_BATCH_UPDATE_STATEMENT, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - long[] updateCounts = txContext.batchUpdate(updateStatements); - createAndAddRetriableBatchUpdate(updateStatements, updateCounts); - return updateCounts; - } catch (AbortedException e) { - throw e; - } catch (SpannerException e) { - createAndAddFailedBatchUpdate(e, updateStatements); - throw e; + + ApiFuture res; + if (retryAbortsInternally) { + res = + executeStatementAsync( + EXECUTE_BATCH_UPDATE_STATEMENT, + new Callable() { + @Override + public long[] call() throws Exception { + return runWithRetry( + new Callable() { + @Override + public long[] call() throws Exception { + try { + getStatementExecutor() + .invokeInterceptors( + EXECUTE_BATCH_UPDATE_STATEMENT, + StatementExecutionStep.EXECUTE_STATEMENT, + ReadWriteTransaction.this); + long[] updateCounts = + get(txContextFuture).batchUpdate(updateStatements); + createAndAddRetriableBatchUpdate(updateStatements, updateCounts); + return updateCounts; + } catch (AbortedException e) { + throw e; + } catch (SpannerException e) { + createAndAddFailedBatchUpdate(e, updateStatements); + throw e; + } } - } - }); - } - }, - InterceptorsUsage - .IGNORE_INTERCEPTORS); // ignore interceptors here as they are invoked in the - // Callable. - } else { - return asyncExecuteStatement( - EXECUTE_BATCH_UPDATE_STATEMENT, - new Callable() { - @Override - public long[] call() throws Exception { - return txContext.batchUpdate(updateStatements); - } - }); - } - } catch (SpannerException e) { - handlePossibleInvalidatingException(e); - throw e; + }); + } + }, + // ignore interceptors here as they are invoked in the Callable. + InterceptorsUsage.IGNORE_INTERCEPTORS, + ImmutableList.>of(SpannerGrpc.getExecuteBatchDmlMethod())); + } else { + res = + executeStatementAsync( + EXECUTE_BATCH_UPDATE_STATEMENT, + new Callable() { + @Override + public long[] call() throws Exception { + checkAborted(); + return get(txContextFuture).batchUpdate(updateStatements); + } + }, + SpannerGrpc.getExecuteBatchDmlMethod()); } - } - @Override - public void write(Mutation mutation) { - Preconditions.checkNotNull(mutation); - checkValidTransaction(); - mutations.add(mutation); + ApiFutures.addCallback( + res, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + if (t instanceof SpannerException) { + handlePossibleInvalidatingException((SpannerException) t); + } + } + + @Override + public void onSuccess(long[] result) {} + }, + MoreExecutors.directExecutor()); + return res; } @Override - public void write(Iterable mutations) { + public ApiFuture writeAsync(Iterable mutations) { Preconditions.checkNotNull(mutations); checkValidTransaction(); for (Mutation mutation : mutations) { this.mutations.add(checkNotNull(mutation)); } + return ApiFutures.immediateFuture(null); } /** @@ -440,51 +537,79 @@ public void write(Iterable mutations) { new Callable() { @Override public Void call() throws Exception { - txContext.buffer(mutations); + checkAborted(); + get(txContextFuture).buffer(mutations); txManager.commit(); + commitTimestampFuture.set(txManager.getCommitTimestamp()); + state = UnitOfWorkState.COMMITTED; return null; } }; @Override - public void commit() { + public ApiFuture commitAsync() { checkValidTransaction(); - try { - if (retryAbortsInternally) { - asyncExecuteStatement( - COMMIT_STATEMENT, - new Callable() { - @Override - public Void call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public Void call() throws Exception { - getStatementExecutor() - .invokeInterceptors( - COMMIT_STATEMENT, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - commitCallable.call(); - return null; - } - }); - } - }, - InterceptorsUsage.IGNORE_INTERCEPTORS); - } else { - asyncExecuteStatement(COMMIT_STATEMENT, commitCallable); - } - ReadWriteTransaction.this.state = UnitOfWorkState.COMMITTED; - } catch (SpannerException e) { - try { - txManager.close(); - } catch (Throwable t) { - // ignore - } - this.state = UnitOfWorkState.COMMIT_FAILED; - throw e; + state = UnitOfWorkState.COMMITTING; + commitTimestampFuture = SettableApiFuture.create(); + ApiFuture res; + if (retryAbortsInternally) { + res = + executeStatementAsync( + COMMIT_STATEMENT, + new Callable() { + @Override + public Void call() throws Exception { + try { + return runWithRetry( + new Callable() { + @Override + public Void call() throws Exception { + getStatementExecutor() + .invokeInterceptors( + COMMIT_STATEMENT, + StatementExecutionStep.EXECUTE_STATEMENT, + ReadWriteTransaction.this); + return commitCallable.call(); + } + }); + } catch (Throwable t) { + commitTimestampFuture.setException(t); + state = UnitOfWorkState.COMMIT_FAILED; + try { + txManager.close(); + } catch (Throwable t2) { + // Ignore. + } + throw t; + } + } + }, + InterceptorsUsage.IGNORE_INTERCEPTORS, + ImmutableList.>of(SpannerGrpc.getCommitMethod())); + } else { + res = + executeStatementAsync( + COMMIT_STATEMENT, + new Callable() { + @Override + public Void call() throws Exception { + try { + return commitCallable.call(); + } catch (Throwable t) { + commitTimestampFuture.setException(t); + state = UnitOfWorkState.COMMIT_FAILED; + try { + txManager.close(); + } catch (Throwable t2) { + // Ignore. + } + throw t; + } + } + }, + SpannerGrpc.getCommitMethod()); } + return res; } /** @@ -508,18 +633,17 @@ public Void call() throws Exception { */ T runWithRetry(Callable callable) throws SpannerException { while (true) { - try { - return callable.call(); - } catch (final AbortedException aborted) { - if (retryAbortsInternally) { + synchronized (abortedLock) { + checkAborted(); + try { + return callable.call(); + } catch (final AbortedException aborted) { handleAborted(aborted); - } else { - throw aborted; + } catch (SpannerException e) { + throw e; + } catch (Exception e) { + throw SpannerExceptionFactory.asSpannerException(e); } - } catch (SpannerException e) { - throw e; - } catch (Exception e) { - throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, e.getMessage(), e); } } } @@ -609,7 +733,7 @@ private void handleAborted(AbortedException aborted) { ErrorCode.CANCELLED, "The statement was cancelled"); } try { - txContext = txManager.resetForRetry(); + txContextFuture = ApiFutures.immediateFuture(txManager.resetForRetry()); // Inform listeners about the transaction retry that is about to start. invokeTransactionRetryListenersOnStart(); // Then retry all transaction statements. @@ -630,13 +754,14 @@ private void handleAborted(AbortedException aborted) { RetryResult.RETRY_ABORTED_DUE_TO_CONCURRENT_MODIFICATION); logger.fine( toString() + ": Internal transaction retry aborted due to a concurrent modification"); - // Try to rollback the new transaction and ignore any exceptions. + // Do a shoot and forget rollback. try { txManager.rollback(); } catch (Throwable t) { // ignore } this.state = UnitOfWorkState.ABORTED; + this.abortedException = e; throw e; } catch (AbortedException e) { // Retry aborted, do another retry of the transaction. @@ -651,7 +776,7 @@ private void handleAborted(AbortedException aborted) { Level.FINE, toString() + ": Internal transaction retry failed due to an unexpected exception", e); - // Try to rollback the new transaction and ignore any exceptions. + // Do a shoot and forget rollback. try { txManager.rollback(); } catch (Throwable t) { @@ -659,6 +784,7 @@ private void handleAborted(AbortedException aborted) { } // Set transaction state to aborted as the retry failed. this.state = UnitOfWorkState.ABORTED; + this.abortedException = aborted; // Re-throw underlying exception. throw e; } @@ -671,6 +797,7 @@ private void handleAborted(AbortedException aborted) { } // Internal retry is not enabled. this.state = UnitOfWorkState.ABORTED; + this.abortedException = aborted; throw aborted; } } @@ -689,8 +816,11 @@ private void throwAbortWithRetryAttemptsExceeded() throws SpannerException { // ignore } this.state = UnitOfWorkState.ABORTED; - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.ABORTED, MAX_INTERNAL_RETRIES_EXCEEDED); + this.abortedException = + (AbortedException) + SpannerExceptionFactory.newSpannerException( + ErrorCode.ABORTED, MAX_INTERNAL_RETRIES_EXCEEDED); + throw this.abortedException; } private void invokeTransactionRetryListenersOnStart() { @@ -713,26 +843,30 @@ private void invokeTransactionRetryListenersOnFinish(RetryResult result) { new Callable() { @Override public Void call() throws Exception { - txManager.rollback(); - return null; + try { + if (state != UnitOfWorkState.ABORTED) { + // Make sure the transaction has actually started before we try to rollback. + get(txContextFuture); + txManager.rollback(); + } + return null; + } finally { + txManager.close(); + } } }; @Override - public void rollback() { + public ApiFuture rollbackAsync() { ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, "This transaction has status " + state.name()); - try { - asyncExecuteStatement(rollbackStatement, rollbackCallable); - } finally { - // Whatever happens, we should always call close in order to return the underlying session to - // the session pool to avoid any session leaks. - try { - txManager.close(); - } catch (Throwable e) { - // ignore - } - this.state = UnitOfWorkState.ROLLED_BACK; + state == UnitOfWorkState.STARTED || state == UnitOfWorkState.ABORTED, + "This transaction has status " + state.name()); + state = UnitOfWorkState.ROLLED_BACK; + if (txContextFuture != null && state != UnitOfWorkState.ABORTED) { + return executeStatementAsync( + rollbackStatement, rollbackCallable, SpannerGrpc.getRollbackMethod()); + } else { + return ApiFutures.immediateFuture(null); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java index 614d0c61e52..52011eb9100 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java @@ -16,15 +16,17 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedException; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.ReadOnlyTransaction; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerApiFutures; import com.google.cloud.spanner.SpannerBatchUpdateException; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; @@ -36,13 +38,15 @@ import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.cloud.spanner.connection.StatementParser.StatementType; +import com.google.common.base.Function; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.spanner.admin.database.v1.DatabaseAdminGrpc; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +import com.google.spanner.v1.SpannerGrpc; +import io.grpc.MethodDescriptor; import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; /** * Transaction that is used when a {@link Connection} is in autocommit mode. Each method on this @@ -66,11 +70,11 @@ class SingleUseTransaction extends AbstractBaseUnitOfWork { private final DatabaseClient dbClient; private final TimestampBound readOnlyStaleness; private final AutocommitDmlMode autocommitDmlMode; - private Timestamp readTimestamp = null; + private volatile SettableApiFuture readTimestamp = null; private volatile TransactionManager txManager; - private TransactionRunner writeTransaction; + private volatile TransactionRunner writeTransaction; private boolean used = false; - private UnitOfWorkState state = UnitOfWorkState.STARTED; + private volatile UnitOfWorkState state = UnitOfWorkState.STARTED; static class Builder extends AbstractBaseUnitOfWork.Builder { private DdlClient ddlClient; @@ -160,7 +164,7 @@ private void checkAndMarkUsed() { } @Override - public ResultSet executeQuery( + public ApiFuture executeQueryAsync( final ParsedStatement statement, final AnalyzeMode analyzeMode, final QueryOption... options) { @@ -185,42 +189,43 @@ public ResultSet call() throws Exception { } // Return a DirectExecuteResultSet, which will directly do a next() call in order to // ensure that the query is actually sent to Spanner. - return DirectExecuteResultSet.ofResultSet(rs); - } finally { + ResultSet directRs = DirectExecuteResultSet.ofResultSet(rs); + state = UnitOfWorkState.COMMITTED; + readTimestamp.set(currentTransaction.getReadTimestamp()); + return directRs; + } catch (Throwable t) { + state = UnitOfWorkState.COMMIT_FAILED; + readTimestamp.set(null); currentTransaction.close(); + throw t; } } }; - try { - ResultSet res = asyncExecuteStatement(statement, callable); - readTimestamp = currentTransaction.getReadTimestamp(); - state = UnitOfWorkState.COMMITTED; - return res; - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } finally { - currentTransaction.close(); - } + readTimestamp = SettableApiFuture.create(); + ApiFuture res = + executeStatementAsync(statement, callable, SpannerGrpc.getExecuteStreamingSqlMethod()); + return res; } @Override public Timestamp getReadTimestamp() { ConnectionPreconditions.checkState( - readTimestamp != null, "There is no read timestamp available for this transaction."); - return readTimestamp; + SpannerApiFutures.getOrNull(readTimestamp) != null, + "There is no read timestamp available for this transaction."); + return SpannerApiFutures.get(readTimestamp); } @Override public Timestamp getReadTimestampOrNull() { - return readTimestamp; + return SpannerApiFutures.getOrNull(readTimestamp); } private boolean hasCommitTimestamp() { - return writeTransaction != null - || (txManager != null - && txManager.getState() - == com.google.cloud.spanner.TransactionManager.TransactionState.COMMITTED); + return state == UnitOfWorkState.COMMITTED + && (writeTransaction != null + || (txManager != null + && txManager.getState() + == com.google.cloud.spanner.TransactionManager.TransactionState.COMMITTED)); } @Override @@ -247,7 +252,7 @@ public Timestamp getCommitTimestampOrNull() { } @Override - public void executeDdl(final ParsedStatement ddl) { + public ApiFuture executeDdlAsync(final ParsedStatement ddl) { Preconditions.checkNotNull(ddl); Preconditions.checkArgument( ddl.getType() == StatementType.DDL, "Statement is not a ddl statement"); @@ -255,70 +260,53 @@ public void executeDdl(final ParsedStatement ddl) { !isReadOnly(), "DDL statements are not allowed in read-only mode"); checkAndMarkUsed(); - try { - Callable callable = - new Callable() { - @Override - public Void call() throws Exception { + Callable callable = + new Callable() { + @Override + public Void call() throws Exception { + try { OperationFuture operation = ddlClient.executeDdl(ddl.getSqlWithoutComments()); - return operation.get(); + Void res = getWithStatementTimeout(operation, ddl); + state = UnitOfWorkState.COMMITTED; + return res; + } catch (Throwable t) { + state = UnitOfWorkState.COMMIT_FAILED; + throw t; } - }; - asyncExecuteStatement(ddl, callable); - state = UnitOfWorkState.COMMITTED; - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } + } + }; + return executeStatementAsync(ddl, callable, DatabaseAdminGrpc.getUpdateDatabaseDdlMethod()); } @Override - public long executeUpdate(final ParsedStatement update) { + public ApiFuture executeUpdateAsync(ParsedStatement update) { Preconditions.checkNotNull(update); Preconditions.checkArgument(update.isUpdate(), "Statement is not an update statement"); ConnectionPreconditions.checkState( !isReadOnly(), "Update statements are not allowed in read-only mode"); checkAndMarkUsed(); - long res; - try { - switch (autocommitDmlMode) { - case TRANSACTIONAL: - res = executeAsyncTransactionalUpdate(update, new TransactionalUpdateCallable(update)); - break; - case PARTITIONED_NON_ATOMIC: - res = executeAsyncPartitionedUpdate(update); - break; - default: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode); - } - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; + ApiFuture res; + switch (autocommitDmlMode) { + case TRANSACTIONAL: + res = executeTransactionalUpdateAsync(update); + break; + case PARTITIONED_NON_ATOMIC: + res = executePartitionedUpdateAsync(update); + break; + default: + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode); } - state = UnitOfWorkState.COMMITTED; return res; } - /** Execute an update statement as a partitioned DML statement. */ - private long executeAsyncPartitionedUpdate(final ParsedStatement update) { - Callable callable = - new Callable() { - @Override - public Long call() throws Exception { - return dbClient.executePartitionedUpdate(update.getStatement()); - } - }; - return asyncExecuteStatement(update, callable); - } - private final ParsedStatement executeBatchUpdateStatement = StatementParser.INSTANCE.parse(Statement.of("RUN BATCH")); @Override - public long[] executeBatchUpdate(Iterable updates) { + public ApiFuture executeBatchUpdateAsync(Iterable updates) { Preconditions.checkNotNull(updates); for (ParsedStatement update : updates) { Preconditions.checkArgument( @@ -329,170 +317,157 @@ public long[] executeBatchUpdate(Iterable updates) { !isReadOnly(), "Batch update statements are not allowed in read-only mode"); checkAndMarkUsed(); - long[] res; - try { - switch (autocommitDmlMode) { - case TRANSACTIONAL: - res = - executeAsyncTransactionalUpdate( - executeBatchUpdateStatement, new TransactionalBatchUpdateCallable(updates)); - break; - case PARTITIONED_NON_ATOMIC: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "Batch updates are not allowed in " + autocommitDmlMode); - default: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode); - } - } catch (SpannerBatchUpdateException e) { - // Batch update exceptions does not cause a rollback. - state = UnitOfWorkState.COMMITTED; - throw e; - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; + switch (autocommitDmlMode) { + case TRANSACTIONAL: + return executeTransactionalBatchUpdateAsync(updates); + case PARTITIONED_NON_ATOMIC: + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, "Batch updates are not allowed in " + autocommitDmlMode); + default: + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode); } - state = UnitOfWorkState.COMMITTED; - return res; } - /** Base class for executing DML updates (both single statements and batches). */ - private abstract class AbstractUpdateCallable implements Callable { - abstract T executeUpdate(TransactionContext txContext); - - @Override - public T call() throws Exception { - try { - txManager = dbClient.transactionManager(); - // Check the interrupted state after each (possible) round-trip to the db to allow the - // statement to be cancelled. - checkInterrupted(); - try (TransactionContext txContext = txManager.begin()) { - checkInterrupted(); - T res = executeUpdate(txContext); - checkInterrupted(); - txManager.commit(); - checkInterrupted(); - return res; - } - } finally { - if (txManager != null) { - // Calling txManager.close() will rollback the transaction if it is still active, i.e. if - // an error occurred before the commit() call returned successfully. - txManager.close(); - } - } - } - } - - /** {@link Callable} for a single update statement. */ - private final class TransactionalUpdateCallable extends AbstractUpdateCallable { - private final ParsedStatement update; - - private TransactionalUpdateCallable(ParsedStatement update) { - this.update = update; - } - - @Override - Long executeUpdate(TransactionContext txContext) { - return txContext.executeUpdate(update.getStatement()); - } - } - - /** {@link Callable} for a batch update. */ - private final class TransactionalBatchUpdateCallable extends AbstractUpdateCallable { - private final List updates; - - private TransactionalBatchUpdateCallable(Iterable updates) { - this.updates = new LinkedList<>(); - for (ParsedStatement update : updates) { - this.updates.add(update.getStatement()); - } - } - - @Override - long[] executeUpdate(TransactionContext txContext) { - return txContext.batchUpdate(updates); - } + private ApiFuture executeTransactionalUpdateAsync(final ParsedStatement update) { + Callable callable = + new Callable() { + @Override + public Long call() throws Exception { + try { + writeTransaction = dbClient.readWriteTransaction(); + Long res = + writeTransaction.run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(update.getStatement()); + } + }); + state = UnitOfWorkState.COMMITTED; + return res; + } catch (Throwable t) { + state = UnitOfWorkState.COMMIT_FAILED; + throw t; + } + } + }; + return executeStatementAsync( + update, + callable, + ImmutableList.>of( + SpannerGrpc.getExecuteSqlMethod(), SpannerGrpc.getCommitMethod())); } - private T executeAsyncTransactionalUpdate( - final ParsedStatement update, final AbstractUpdateCallable callable) { - long startedTime = System.currentTimeMillis(); - // This method uses a TransactionManager instead of the TransactionRunner in order to be able to - // handle timeouts and canceling of a statement. - while (true) { - try { - return asyncExecuteStatement(update, callable); - } catch (AbortedException e) { - try { - Thread.sleep(e.getRetryDelayInMillis() / 1000); - } catch (InterruptedException e1) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.CANCELLED, "Statement execution was interrupted", e1); - } - // Check whether the timeout time has been exceeded. - long executionTime = System.currentTimeMillis() - startedTime; - if (getStatementTimeout().hasTimeout() - && executionTime > getStatementTimeout().getTimeoutValue(TimeUnit.MILLISECONDS)) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.DEADLINE_EXCEEDED, - "Statement execution timeout occurred for " + update.getSqlWithoutComments()); - } - } - } + private ApiFuture executePartitionedUpdateAsync(final ParsedStatement update) { + Callable callable = + new Callable() { + @Override + public Long call() throws Exception { + try { + Long res = dbClient.executePartitionedUpdate(update.getStatement()); + state = UnitOfWorkState.COMMITTED; + return res; + } catch (Throwable t) { + state = UnitOfWorkState.COMMIT_FAILED; + throw t; + } + } + }; + return executeStatementAsync(update, callable, SpannerGrpc.getExecuteStreamingSqlMethod()); } - private void checkInterrupted() throws InterruptedException { - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } + private ApiFuture executeTransactionalBatchUpdateAsync( + final Iterable updates) { + Callable callable = + new Callable() { + @Override + public long[] call() throws Exception { + writeTransaction = dbClient.readWriteTransaction(); + return writeTransaction.run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + try { + long[] res = + transaction.batchUpdate( + Iterables.transform( + updates, + new Function() { + @Override + public Statement apply(ParsedStatement input) { + return input.getStatement(); + } + })); + state = UnitOfWorkState.COMMITTED; + return res; + } catch (Throwable t) { + if (t instanceof SpannerBatchUpdateException) { + // Batch update exceptions does not cause a rollback. + state = UnitOfWorkState.COMMITTED; + } else { + state = UnitOfWorkState.COMMIT_FAILED; + } + throw t; + } + } + }); + } + }; + return executeStatementAsync( + executeBatchUpdateStatement, callable, SpannerGrpc.getExecuteBatchDmlMethod()); } - @Override - public void write(final Mutation mutation) { - write(Arrays.asList(mutation)); - } + private final ParsedStatement commitStatement = + StatementParser.INSTANCE.parse(Statement.of("COMMIT")); @Override - public void write(final Iterable mutations) { + public ApiFuture writeAsync(final Iterable mutations) { Preconditions.checkNotNull(mutations); ConnectionPreconditions.checkState( !isReadOnly(), "Update statements are not allowed in read-only mode"); checkAndMarkUsed(); - writeTransaction = dbClient.readWriteTransaction(); - try { - writeTransaction.run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) throws Exception { - transaction.buffer(mutations); - return null; + Callable callable = + new Callable() { + @Override + public Void call() throws Exception { + try { + writeTransaction = dbClient.readWriteTransaction(); + Void res = + writeTransaction.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.buffer(mutations); + return null; + } + }); + state = UnitOfWorkState.COMMITTED; + return res; + } catch (Throwable t) { + state = UnitOfWorkState.COMMIT_FAILED; + throw t; } - }); - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } - state = UnitOfWorkState.COMMITTED; + } + }; + return executeStatementAsync(commitStatement, callable, SpannerGrpc.getCommitMethod()); } @Override - public void commit() { + public ApiFuture commitAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Commit is not supported for single-use transactions"); } @Override - public void rollback() { + public ApiFuture rollbackAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Rollback is not supported for single-use transactions"); } @Override - public long[] runBatch() { + public ApiFuture runBatchAsync() { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for single-use transactions"); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index ecf13cd399f..350cf61394e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -28,6 +28,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; +import com.google.common.base.Ticker; import com.google.common.collect.Iterables; import io.grpc.ManagedChannelBuilder; import java.util.ArrayList; @@ -80,7 +81,7 @@ public static void closeSpannerPool() { private static final long DEFAULT_CLOSE_SPANNER_AFTER_MILLISECONDS_UNUSED = 60000L; static final SpannerPool INSTANCE = - new SpannerPool(DEFAULT_CLOSE_SPANNER_AFTER_MILLISECONDS_UNUSED); + new SpannerPool(DEFAULT_CLOSE_SPANNER_AFTER_MILLISECONDS_UNUSED, Ticker.systemTicker()); @VisibleForTesting enum CheckAndCloseSpannersMode { @@ -236,14 +237,17 @@ public int hashCode() { @GuardedBy("this") private final Map lastConnectionClosedAt = new HashMap<>(); + private final Ticker ticker; + @VisibleForTesting - SpannerPool() { - this(0L); + SpannerPool(Ticker ticker) { + this(0L, ticker); } @VisibleForTesting - SpannerPool(long closeSpannerAfterMillisecondsUnused) { + SpannerPool(long closeSpannerAfterMillisecondsUnused, Ticker ticker) { this.closeSpannerAfterMillisecondsUnused = closeSpannerAfterMillisecondsUnused; + this.ticker = ticker; } /** @@ -333,6 +337,9 @@ public ManagedChannelBuilder apply(ManagedChannelBuilder input) { } }); } + if (options.getConfigurator() != null) { + options.getConfigurator().configure(builder); + } return builder.build().getService(); } @@ -360,7 +367,8 @@ void removeConnection(ConnectionOptions options, ConnectionImpl connection) { if (registeredConnections.isEmpty()) { // Register the moment the last connection for this Spanner key was removed, so we know // which Spanner objects we could close. - lastConnectionClosedAt.put(key, System.currentTimeMillis()); + lastConnectionClosedAt.put( + key, TimeUnit.MILLISECONDS.convert(ticker.read(), TimeUnit.NANOSECONDS)); } } } else { @@ -443,7 +451,8 @@ void closeUnusedSpanners(long closeSpannerAfterMillisecondsUnused) { // Check whether the last connection was closed more than // closeSpannerAfterMillisecondsUnused milliseconds ago. if (closedAt != null - && ((System.currentTimeMillis() - closedAt.longValue())) + && ((TimeUnit.MILLISECONDS.convert(ticker.read(), TimeUnit.NANOSECONDS) + - closedAt.longValue())) > closeSpannerAfterMillisecondsUnused) { Spanner spanner = spanners.get(entry.getKey()); if (spanner != null) { @@ -463,4 +472,11 @@ void closeUnusedSpanners(long closeSpannerAfterMillisecondsUnused) { } } } + + @VisibleForTesting + int getCurrentSpannerCount() { + synchronized (this) { + return spanners.size(); + } + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementExecutor.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementExecutor.java index bb1fa281264..baaadbe1676 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementExecutor.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementExecutor.java @@ -16,10 +16,13 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; +import com.google.api.core.ListenableFutureToApiFuture; import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.DurationValueGetter; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.protobuf.Duration; @@ -27,11 +30,11 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.threeten.bp.temporal.ChronoUnit; /** * {@link StatementExecutor} is responsible for executing statements on a {@link Connection}. @@ -55,23 +58,7 @@ static boolean isValidTimeoutUnit(TimeUnit unit) { } /** The statement timeout. */ - private Duration duration = null; - - /** Creates a {@link StatementTimeout} that will never timeout. */ - @VisibleForTesting - static StatementTimeout nullTimeout() { - return new StatementTimeout(); - } - - /** Creates a {@link StatementTimeout} with the given duration. */ - @VisibleForTesting - static StatementTimeout of(long timeout, TimeUnit unit) { - Preconditions.checkArgument(timeout > 0L); - Preconditions.checkArgument(isValidTimeoutUnit(unit)); - StatementTimeout res = new StatementTimeout(); - res.duration = ReadOnlyStalenessUtil.createDuration(timeout, unit); - return res; - } + private volatile Duration duration = null; /** * Does this {@link StatementTimeout} have an actual timeout (i.e. it will eventually timeout). @@ -115,6 +102,31 @@ public boolean hasDuration() { } }); } + + org.threeten.bp.Duration asDuration() { + if (!hasTimeout()) { + return org.threeten.bp.Duration.ZERO; + } + TimeUnit unit = getAppropriateTimeUnit(); + switch (unit) { + case DAYS: + return org.threeten.bp.Duration.ofDays(getTimeoutValue(unit)); + case HOURS: + return org.threeten.bp.Duration.ofHours(getTimeoutValue(unit)); + case MICROSECONDS: + return org.threeten.bp.Duration.of(getTimeoutValue(unit), ChronoUnit.MICROS); + case MILLISECONDS: + return org.threeten.bp.Duration.ofMillis(getTimeoutValue(unit)); + case MINUTES: + return org.threeten.bp.Duration.ofMinutes(getTimeoutValue(unit)); + case NANOSECONDS: + return org.threeten.bp.Duration.ofNanos(getTimeoutValue(unit)); + case SECONDS: + return org.threeten.bp.Duration.ofSeconds(getTimeoutValue(unit)); + default: + throw new IllegalStateException("invalid time unit: " + unit); + } + } } /** @@ -129,12 +141,13 @@ public boolean hasDuration() { .build(); /** Creates an {@link ExecutorService} for a {@link StatementExecutor}. */ - private static ExecutorService createExecutorService() { - return new ThreadPoolExecutor( - 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), THREAD_FACTORY); + private static ListeningExecutorService createExecutorService() { + return MoreExecutors.listeningDecorator( + new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), THREAD_FACTORY)); } - private ExecutorService executor = createExecutorService(); + private ListeningExecutorService executor = createExecutorService(); /** * Interceptors that should be invoked before or after a statement is executed can be registered @@ -151,18 +164,6 @@ private static ExecutorService createExecutorService() { this.interceptors = Collections.unmodifiableList(interceptors); } - /** - * Recreates this {@link StatementExecutor} and its {@link ExecutorService}. This can be necessary - * if a statement times out or is cancelled, and it cannot be guaranteed that the statement - * execution can be terminated. In order to prevent the single threaded {@link ExecutorService} to - * continue to block on the timed out/cancelled statement, a new {@link ExecutorService} is - * created. - */ - void recreate() { - executor.shutdown(); - executor = createExecutorService(); - } - /** * Shutdown this executor now and do not wait for any statement that is being executed to finish. */ @@ -171,8 +172,8 @@ List shutdownNow() { } /** Execute a statement on this {@link StatementExecutor}. */ - Future submit(Callable callable) { - return executor.submit(callable); + ApiFuture submit(Callable callable) { + return new ListenableFutureToApiFuture<>(executor.submit(callable)); } /** diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResultImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResultImpl.java index 6221cc447b6..37e8d7e5a07 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResultImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResultImpl.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; + import com.google.cloud.Timestamp; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; @@ -27,6 +29,27 @@ /** Implementation of {@link StatementResult} */ class StatementResultImpl implements StatementResult { + /** + * Returns the {@link AsyncStatementResult} as a {@link StatementResult} with the guarantee that + * the underlying result is available. + */ + static StatementResult of(AsyncStatementResult delegate) { + switch (delegate.getResultType()) { + case NO_RESULT: + get(delegate.getNoResultAsync()); + break; + case RESULT_SET: + delegate.getResultSet(); + break; + case UPDATE_COUNT: + delegate.getUpdateCount(); + break; + default: + throw new IllegalStateException("Unknown result type: " + delegate.getResultType()); + } + return delegate; + } + /** {@link StatementResult} containing a {@link ResultSet} returned by Cloud Spanner. */ static StatementResult of(ResultSet resultSet) { return new StatementResultImpl(resultSet, null); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java index e372229c64c..eb3c47d4bf1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.connection; +import com.google.api.core.ApiFuture; import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; import com.google.cloud.spanner.Mutation; @@ -26,6 +27,7 @@ import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.spanner.v1.ResultSetStats; +import java.util.concurrent.ExecutionException; /** Internal interface for transactions and batches on {@link Connection}s. */ @InternalApi @@ -39,9 +41,11 @@ enum Type { enum UnitOfWorkState { STARTED, + COMMITTING, COMMITTED, COMMIT_FAILED, ROLLED_BACK, + RUNNING, RAN, RUN_FAILED, ABORTED; @@ -67,30 +71,35 @@ public boolean isActive() { * Commits the changes in this unit of work to the database. For read-only transactions, this only * closes the {@link ReadContext}. This method will throw a {@link SpannerException} if called for * a {@link Type#BATCH}. + * + * @return An {@link ApiFuture} that is done when the commit has finished. */ - void commit(); + ApiFuture commitAsync(); /** * Rollbacks any changes in this unit of work. For read-only transactions, this only closes the * {@link ReadContext}. This method will throw a {@link SpannerException} if called for a {@link * Type#BATCH}. + * + * @return An {@link ApiFuture} that is done when the rollback has finished. */ - void rollback(); + ApiFuture rollbackAsync(); /** * Sends the currently buffered statements in this unit of work to the database and ends the * batch. This method will throw a {@link SpannerException} if called for a {@link * Type#TRANSACTION}. * - * @return the update counts in case of a DML batch. Returns an array containing 1 for each - * successful statement and 0 for each failed statement or statement that was not executed DDL - * in case of a DDL batch. + * @return an {@link ApiFuture} containing the update counts in case of a DML batch. Returns an + * array containing 1 for each successful statement and 0 for each failed statement or + * statement that was not executed in case of a DDL batch. */ - long[] runBatch(); + ApiFuture runBatchAsync(); /** * Clears the currently buffered statements in this unit of work and ends the batch. This method - * will throw a {@link SpannerException} if called for a {@link Type#TRANSACTION}. + * will throw a {@link SpannerException} if called for a {@link Type#TRANSACTION}. This method is + * always non-blocking. */ void abortBatch(); @@ -107,11 +116,12 @@ public boolean isActive() { * ResultSet} or not. Cannot be used in combination with {@link QueryOption}s. * @param options the options to configure the query. May only be set if analyzeMode is set to * {@link AnalyzeMode#NONE}. - * @return a {@link ResultSet} with the results of the query. - * @throws SpannerException if the query is not allowed on this {@link UnitOfWork}, or if a - * database error occurs. + * @return an {@link ApiFuture} containing a {@link ResultSet} with the results of the query. + * @throws SpannerException if the query is not allowed on this {@link UnitOfWork}. The {@link + * ApiFuture} will return a {@link SpannerException} wrapped in an {@link ExecutionException} + * if a database error occurs. */ - ResultSet executeQuery( + ApiFuture executeQueryAsync( ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options); /** @@ -139,36 +149,28 @@ ResultSet executeQuery( * statement directly on Spanner. * * @param ddl The DDL statement to execute. + * @return an {@link ApiFuture} that is done when the DDL operation has finished. */ - void executeDdl(ParsedStatement ddl); + ApiFuture executeDdlAsync(ParsedStatement ddl); /** * Execute a DML statement on Spanner. * * @param update The DML statement to execute. - * @return the number of records that were inserted/updated/deleted by this statement. + * @return an {@link ApiFuture} containing the number of records that were + * inserted/updated/deleted by this statement. */ - long executeUpdate(ParsedStatement update); + ApiFuture executeUpdateAsync(ParsedStatement update); /** * Execute a batch of DML statements on Spanner. * * @param updates The DML statements to execute. - * @return an array containing the number of records that were inserted/updated/deleted per - * statement. + * @return an {@link ApiFuture} containing an array with the number of records that were + * inserted/updated/deleted per statement. * @see TransactionContext#batchUpdate(Iterable) */ - long[] executeBatchUpdate(Iterable updates); - - /** - * Writes a {@link Mutation} to Spanner. For {@link ReadWriteTransaction}s, this means buffering - * the {@link Mutation} locally and writing the {@link Mutation} to Spanner upon {@link - * UnitOfWork#commit()}. For {@link SingleUseTransaction}s, the {@link Mutation} will be sent - * directly to Spanner. - * - * @param mutation The mutation to write. - */ - void write(Mutation mutation); + ApiFuture executeBatchUpdateAsync(Iterable updates); /** * Writes a batch of {@link Mutation}s to Spanner. For {@link ReadWriteTransaction}s, this means @@ -177,6 +179,8 @@ ResultSet executeQuery( * sent directly to Spanner. * * @param mutations The mutations to write. + * @return an {@link ApiFuture} that is done when the {@link Mutation}s have been successfully + * buffered or written to Cloud Spanner. */ - void write(Iterable mutations); + ApiFuture writeAsync(Iterable mutations); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index cae41510fca..4f55cd5ebd7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -212,6 +212,7 @@ public PartialResultSet next() { int recordCount = 0; while (recordCount < MAX_ROWS_IN_CHUNK && currentRow < resultSet.getRowsCount()) { builder.addAllValues(resultSet.getRows(currentRow).getValuesList()); + builder.setResumeToken(ByteString.copyFromUtf8(String.format("%010d", currentRow))); recordCount++; currentRow++; } @@ -408,6 +409,7 @@ public static class SimulatedExecutionTime { private final int randomExecutionTime; private final Queue exceptions; private final boolean stickyException; + private final Queue streamIndices; /** * Creates a simulated execution time that will always be somewhere between @@ -430,11 +432,18 @@ public static SimulatedExecutionTime none() { } public static SimulatedExecutionTime ofException(Exception exception) { - return new SimulatedExecutionTime(0, 0, Arrays.asList(exception), false); + return new SimulatedExecutionTime( + 0, 0, Arrays.asList(exception), false, Collections.emptySet()); } public static SimulatedExecutionTime ofStickyException(Exception exception) { - return new SimulatedExecutionTime(0, 0, Arrays.asList(exception), true); + return new SimulatedExecutionTime( + 0, 0, Arrays.asList(exception), true, Collections.emptySet()); + } + + public static SimulatedExecutionTime ofStreamException(Exception exception, long streamIndex) { + return new SimulatedExecutionTime( + 0, 0, Arrays.asList(exception), false, Collections.singleton(streamIndex)); } public static SimulatedExecutionTime stickyDatabaseNotFoundException(String name) { @@ -443,27 +452,37 @@ public static SimulatedExecutionTime stickyDatabaseNotFoundException(String name } public static SimulatedExecutionTime ofExceptions(Collection exceptions) { - return new SimulatedExecutionTime(0, 0, exceptions, false); + return new SimulatedExecutionTime(0, 0, exceptions, false, Collections.emptySet()); } public static SimulatedExecutionTime ofMinimumAndRandomTimeAndExceptions( int minimumExecutionTime, int randomExecutionTime, Collection exceptions) { return new SimulatedExecutionTime( - minimumExecutionTime, randomExecutionTime, exceptions, false); + minimumExecutionTime, + randomExecutionTime, + exceptions, + false, + Collections.emptySet()); } private SimulatedExecutionTime(int minimum, int random) { - this(minimum, random, Collections.emptyList(), false); + this( + minimum, random, Collections.emptyList(), false, Collections.emptySet()); } private SimulatedExecutionTime( - int minimum, int random, Collection exceptions, boolean stickyException) { + int minimum, + int random, + Collection exceptions, + boolean stickyException, + Collection streamIndices) { Preconditions.checkArgument(minimum >= 0, "Minimum execution time must be >= 0"); Preconditions.checkArgument(random >= 0, "Random execution time must be >= 0"); this.minimumExecutionTime = minimum; this.randomExecutionTime = random; this.exceptions = new LinkedList<>(exceptions); this.stickyException = stickyException; + this.streamIndices = new LinkedList<>(streamIndices); } void simulateExecutionTime( @@ -472,7 +491,9 @@ void simulateExecutionTime( CountDownLatch freezeLock) { Uninterruptibles.awaitUninterruptibly(freezeLock); checkException(globalExceptions, stickyGlobalExceptions); - checkException(this.exceptions, stickyException); + if (streamIndices.isEmpty()) { + checkException(this.exceptions, stickyException); + } if (minimumExecutionTime > 0 || randomExecutionTime > 0) { Uninterruptibles.sleepUninterruptibly( (randomExecutionTime == 0 ? 0 : RANDOM.nextInt(randomExecutionTime)) @@ -488,6 +509,18 @@ private static void checkException(Queue exceptions, boolean keepExce throw Status.INTERNAL.withDescription(e.getMessage()).withCause(e).asRuntimeException(); } } + + private static void checkStreamException( + long streamIndex, Queue exceptions, Queue streamIndices) { + Exception e = exceptions.peek(); + Long index = streamIndices.peek(); + if (e != null && index != null && index == streamIndex) { + exceptions.poll(); + streamIndices.poll(); + Throwables.throwIfUnchecked(e); + throw Status.INTERNAL.withDescription(e.getMessage()).withCause(e).asRuntimeException(); + } + } } public static final SimulatedExecutionTime NO_EXECUTION_TIME = SimulatedExecutionTime.none(); @@ -1096,7 +1129,11 @@ public void executeStreamingSql( throw res.getException(); case RESULT_SET: returnPartialResultSet( - res.getResultSet(), transactionId, request.getTransaction(), responseObserver); + res.getResultSet(), + transactionId, + request.getTransaction(), + responseObserver, + getExecuteStreamingSqlExecutionTime()); break; case UPDATE_COUNT: if (isPartitioned) { @@ -1425,7 +1462,11 @@ public Iterator iterator() { .asRuntimeException(); } returnPartialResultSet( - res.getResultSet(), transactionId, request.getTransaction(), responseObserver); + res.getResultSet(), + transactionId, + request.getTransaction(), + responseObserver, + getStreamingReadExecutionTime()); } catch (StatusRuntimeException e) { responseObserver.onError(e); } catch (Throwable t) { @@ -1437,7 +1478,8 @@ private void returnPartialResultSet( ResultSet resultSet, ByteString transactionId, TransactionSelector transactionSelector, - StreamObserver responseObserver) { + StreamObserver responseObserver, + SimulatedExecutionTime executionTime) { ResultSetMetadata metadata = resultSet.getMetadata(); if (transactionId == null) { Transaction transaction = getTemporaryTransactionOrNull(transactionSelector); @@ -1451,8 +1493,12 @@ private void returnPartialResultSet( } resultSet = resultSet.toBuilder().setMetadata(metadata).build(); PartialResultSetsIterator iterator = new PartialResultSetsIterator(resultSet); + long index = 0L; while (iterator.hasNext()) { + SimulatedExecutionTime.checkStreamException( + index, executionTime.exceptions, executionTime.streamIndices); responseObserver.onNext(iterator.next()); + index++; } responseObserver.onCompleted(); } @@ -1699,6 +1745,11 @@ public void commit(CommitRequest request, StreamObserver respons .build()); } else if (request.getTransactionId() != null) { transaction = transactions.get(request.getTransactionId()); + Optional aborted = + Optional.fromNullable(abortedTransactions.get(request.getTransactionId())); + if (aborted.or(Boolean.FALSE)) { + throwTransactionAborted(request.getTransactionId()); + } } else { // No transaction mode specified responseObserver.onError( @@ -1864,6 +1915,18 @@ public void waitForLastRequestToBe(Class type, long t } } + public void waitForRequestsToContain(Class type, long timeoutMillis) + throws InterruptedException, TimeoutException { + Stopwatch watch = Stopwatch.createStarted(); + while (countRequestsOfType(type) == 0) { + Thread.sleep(10L); + if (watch.elapsed(TimeUnit.MILLISECONDS) > timeoutMillis) { + throw new TimeoutException( + "Timeout while waiting for requests to contain " + type.getName()); + } + } + } + @Override public void addResponse(AbstractMessage response) { throw new UnsupportedOperationException(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index bd3c0c9c523..2d7d695ea29 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -20,15 +20,24 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.core.ExecutorProvider; import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; import com.google.common.primitives.Booleans; import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; +import com.google.common.util.concurrent.MoreExecutors; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -350,4 +359,132 @@ public void exceptionIfNextIsNotCalled() { assertNotNull(ex.getMessage()); } } + + @Test + public void testToAsyncResultSet() { + ResultSet delegate = + ResultSets.forRows( + Type.struct(Type.StructField.of("f1", Type.string())), + Arrays.asList(Struct.newBuilder().set("f1").to("x").build())); + + final AtomicInteger count = new AtomicInteger(); + AsyncResultSet rs = ResultSets.toAsyncResultSet(delegate); + ApiFuture fut = + rs.setCallback( + MoreExecutors.directExecutor(), + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + count.incrementAndGet(); + assertThat(resultSet.getString("f1")).isEqualTo("x"); + } + } + } + }); + SpannerApiFutures.get(fut); + assertThat(count.get()).isEqualTo(1); + } + + @Test + public void testToAsyncResultSetWithExecProvider() { + ResultSet delegate = + ResultSets.forRows( + Type.struct(Type.StructField.of("f1", Type.string())), + Arrays.asList(Struct.newBuilder().set("f1").to("x").build())); + + ExecutorProvider provider = + new ExecutorProvider() { + final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + @Override + public boolean shouldAutoClose() { + return true; + } + + @Override + public ScheduledExecutorService getExecutor() { + return executor; + } + }; + final AtomicInteger count = new AtomicInteger(); + AsyncResultSet rs = ResultSets.toAsyncResultSet(delegate, provider); + ApiFuture fut = + rs.setCallback( + MoreExecutors.directExecutor(), + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + count.incrementAndGet(); + assertThat(resultSet.getString("f1")).isEqualTo("x"); + } + } + } + }); + SpannerApiFutures.get(fut); + assertThat(count.get()).isEqualTo(1); + assertThat(provider.getExecutor().isShutdown()).isTrue(); + } + + @Test + public void testToAsyncResultSetWithFuture() { + ApiFuture delegateFuture = + ApiFutures.immediateFuture( + ResultSets.forRows( + Type.struct(Type.StructField.of("f1", Type.string())), + Arrays.asList(Struct.newBuilder().set("f1").to("x").build()))); + + ExecutorProvider provider = + new ExecutorProvider() { + final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + @Override + public boolean shouldAutoClose() { + return false; + } + + @Override + public ScheduledExecutorService getExecutor() { + return executor; + } + }; + final AtomicInteger count = new AtomicInteger(); + AsyncResultSet rs = ResultSets.toAsyncResultSet(delegateFuture, provider); + ApiFuture fut = + rs.setCallback( + MoreExecutors.directExecutor(), + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + count.incrementAndGet(); + assertThat(resultSet.getString("f1")).isEqualTo("x"); + } + } + } + }); + SpannerApiFutures.get(fut); + assertThat(count.get()).isEqualTo(1); + assertThat(provider.getExecutor().isShutdown()).isFalse(); + provider.getExecutor().shutdown(); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerApiFuturesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerApiFuturesTest.java new file mode 100644 index 00000000000..8b0d03717a6 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerApiFuturesTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.cloud.spanner.SpannerApiFutures.get; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.ForwardingApiFuture; +import java.util.concurrent.CancellationException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SpannerApiFuturesTest { + + @Test + public void testGet() { + ApiFuture fut = ApiFutures.immediateFuture(1L); + assertThat(get(fut)).isEqualTo(1L); + } + + @Test + public void testGetNull() { + try { + get(null); + fail("Missing expected exception"); + } catch (NullPointerException e) { + // Ignore, this is the expected exception. + } + } + + @Test + public void testGetOrNull() { + assertThat(SpannerApiFutures.getOrNull(null)).isNull(); + } + + @Test + public void testGetSpannerException() { + ApiFuture fut = + ApiFutures.immediateFailedFuture( + SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, "test exception")); + try { + get(fut); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); + assertThat(e.getMessage()).contains("test exception"); + } + } + + @Test + public void testGetOtherException() { + ApiFuture fut = + ApiFutures.immediateFailedFuture(new RuntimeException("test runtime exception")); + try { + get(fut); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); + assertThat(e.getMessage()).contains("test runtime exception"); + } + } + + @Test + public void testGetInterruptedException() { + ApiFuture fut = + new ForwardingApiFuture(ApiFutures.immediateFuture(null)) { + public Void get() throws InterruptedException { + throw new InterruptedException("test interrupted exception"); + } + }; + try { + get(fut); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.CANCELLED); + // The message of an interrupted exception is not included in the SpannerException. + assertThat(e.getMessage()).doesNotContain("test interrupted exception"); + } + } + + @Test + public void testGetCancellationException() { + ApiFuture fut = + new ForwardingApiFuture(ApiFutures.immediateFuture(null)) { + public Void get() throws InterruptedException { + throw new CancellationException("test cancellation exception"); + } + }; + try { + get(fut); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.CANCELLED); + // The message of an cancellation exception is included in the SpannerException. + assertThat(e.getMessage()).contains("test cancellation exception"); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java index a54a5b848ab..b2ebd826615 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java @@ -23,7 +23,13 @@ import com.google.cloud.spanner.admin.instance.v1.MockInstanceAdminImpl; import com.google.cloud.spanner.connection.ITAbstractSpannerTest.AbortInterceptor; import com.google.cloud.spanner.connection.ITAbstractSpannerTest.ITConnection; +import com.google.common.util.concurrent.AbstractFuture; +import com.google.longrunning.GetOperationRequest; +import com.google.longrunning.Operation; +import com.google.longrunning.OperationsGrpc.OperationsImplBase; import com.google.protobuf.AbstractMessage; +import com.google.protobuf.Any; +import com.google.protobuf.Empty; import com.google.protobuf.ListValue; import com.google.protobuf.Value; import com.google.spanner.v1.ExecuteSqlRequest; @@ -33,7 +39,9 @@ import com.google.spanner.v1.Type; import com.google.spanner.v1.TypeCode; import io.grpc.Server; +import io.grpc.internal.LogExceptionRunnable; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.StreamObserver; import java.io.IOException; import java.net.InetSocketAddress; import java.sql.DriverManager; @@ -41,6 +49,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.logging.Logger; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -87,31 +96,58 @@ public abstract class AbstractMockServerTest { .build(); public static final Statement INSERT_STATEMENT = Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')"); - public static final int UPDATE_COUNT = 1; + public static final long UPDATE_COUNT = 1L; + + public static final int RANDOM_RESULT_SET_ROW_COUNT = 100; + public static final Statement SELECT_RANDOM_STATEMENT = Statement.of("SELECT * FROM RANDOM"); + public static final com.google.spanner.v1.ResultSet RANDOM_RESULT_SET = + new RandomResultSetGenerator(RANDOM_RESULT_SET_ROW_COUNT).generate(); public static MockSpannerServiceImpl mockSpanner; public static MockInstanceAdminImpl mockInstanceAdmin; public static MockDatabaseAdminImpl mockDatabaseAdmin; + public static OperationsImplBase mockOperations; private static Server server; private static InetSocketAddress address; + private boolean futureParentHandlers; + private boolean exceptionRunnableParentHandlers; + private boolean nettyServerParentHandlers; + @BeforeClass public static void startStaticServer() throws IOException { mockSpanner = new MockSpannerServiceImpl(); mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. mockInstanceAdmin = new MockInstanceAdminImpl(); mockDatabaseAdmin = new MockDatabaseAdminImpl(); + mockOperations = + new OperationsImplBase() { + @Override + public void getOperation( + GetOperationRequest request, StreamObserver responseObserver) { + responseObserver.onNext( + Operation.newBuilder() + .setDone(false) + .setName(request.getName()) + .setMetadata(Any.pack(Empty.getDefaultInstance())) + .build()); + responseObserver.onCompleted(); + } + }; address = new InetSocketAddress("localhost", 0); server = NettyServerBuilder.forAddress(address) .addService(mockSpanner) .addService(mockInstanceAdmin) .addService(mockDatabaseAdmin) + .addService(mockOperations) .build() .start(); mockSpanner.putStatementResult( StatementResult.query(SELECT_COUNT_STATEMENT, SELECT_COUNT_RESULTSET_BEFORE_INSERT)); mockSpanner.putStatementResult(StatementResult.update(INSERT_STATEMENT, UPDATE_COUNT)); + mockSpanner.putStatementResult( + StatementResult.query(SELECT_RANDOM_STATEMENT, RANDOM_RESULT_SET)); } @AfterClass @@ -124,11 +160,32 @@ public static void stopServer() throws Exception { @Before public void setupResults() { mockSpanner.reset(); + mockDatabaseAdmin.reset(); + mockInstanceAdmin.reset(); + + futureParentHandlers = Logger.getLogger(AbstractFuture.class.getName()).getUseParentHandlers(); + exceptionRunnableParentHandlers = + Logger.getLogger(LogExceptionRunnable.class.getName()).getUseParentHandlers(); + nettyServerParentHandlers = + Logger.getLogger("io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler") + .getUseParentHandlers(); + Logger.getLogger(AbstractFuture.class.getName()).setUseParentHandlers(false); + Logger.getLogger(LogExceptionRunnable.class.getName()).setUseParentHandlers(false); + Logger.getLogger("io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler") + .setUseParentHandlers(false); } @After public void closeSpannerPool() { - SpannerPool.closeSpannerPool(); + try { + SpannerPool.closeSpannerPool(); + } finally { + Logger.getLogger(AbstractFuture.class.getName()).setUseParentHandlers(futureParentHandlers); + Logger.getLogger(LogExceptionRunnable.class.getName()) + .setUseParentHandlers(exceptionRunnableParentHandlers); + Logger.getLogger("io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler") + .setUseParentHandlers(nettyServerParentHandlers); + } } protected java.sql.Connection createJdbcConnection() throws SQLException { @@ -184,7 +241,7 @@ protected ExecuteSqlRequest getLastExecuteSqlRequest() { throw new IllegalStateException("No ExecuteSqlRequest found in requests"); } - private ITConnection createITConnection(ConnectionOptions options) { + ITConnection createITConnection(ConnectionOptions options) { return new ITConnectionImpl(options); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AsyncStatementResultImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AsyncStatementResultImplTest.java new file mode 100644 index 00000000000..53c3e1a1fcb --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AsyncStatementResultImplTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.SpannerApiFutures.get; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import com.google.api.core.ApiFutures; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.connection.StatementResult.ResultType; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AsyncStatementResultImplTest { + + @Test + public void testNoResultGetResultSetAsync() { + AsyncStatementResult subject = + AsyncStatementResultImpl.noResult(ApiFutures.immediateFuture(null)); + assertThat(subject.getResultType()).isEqualTo(ResultType.NO_RESULT); + try { + subject.getResultSetAsync(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); + } + } + + @Test + public void testNoResultGetUpdateCountAsync() { + AsyncStatementResult subject = + AsyncStatementResultImpl.noResult(ApiFutures.immediateFuture(null)); + assertThat(subject.getResultType()).isEqualTo(ResultType.NO_RESULT); + try { + subject.getUpdateCountAsync(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); + } + } + + @Test + public void testResultSetGetResultSetAsync() { + AsyncStatementResult subject = AsyncStatementResultImpl.of(mock(AsyncResultSet.class)); + assertThat(subject.getResultType()).isEqualTo(ResultType.RESULT_SET); + assertThat(subject.getResultSetAsync()).isNotNull(); + } + + @Test + public void testResultSetGetUpdateCountAsync() { + AsyncStatementResult subject = AsyncStatementResultImpl.of(mock(AsyncResultSet.class)); + assertThat(subject.getResultType()).isEqualTo(ResultType.RESULT_SET); + try { + subject.getUpdateCountAsync(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); + } + } + + @Test + public void testUpdateCountGetResultSetAsync() { + AsyncStatementResult subject = AsyncStatementResultImpl.of(ApiFutures.immediateFuture(1L)); + assertThat(subject.getResultType()).isEqualTo(ResultType.UPDATE_COUNT); + try { + subject.getResultSetAsync(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); + } + } + + @Test + public void testUpdateCountGetUpdateCountAsync() { + AsyncStatementResult subject = AsyncStatementResultImpl.of(ApiFutures.immediateFuture(1L)); + assertThat(subject.getResultType()).isEqualTo(ResultType.UPDATE_COUNT); + assertThat(get(subject.getUpdateCountAsync())).isEqualTo(1L); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiAbortedTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiAbortedTest.java new file mode 100644 index 00000000000..a209bfa3122 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiAbortedTest.java @@ -0,0 +1,688 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.SpannerApiFutures.get; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.Options; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.connection.ITAbstractSpannerTest.ITConnection; +import com.google.common.base.Predicate; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.AbstractMessage; +import com.google.spanner.v1.ExecuteSqlRequest; +import io.grpc.Status; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** Tests retry handling of read/write transactions using the Async Connection API. */ +public class ConnectionAsyncApiAbortedTest extends AbstractMockServerTest { + private static final class QueryResult { + final ApiFuture finished; + final AtomicInteger rowCount; + + QueryResult(ApiFuture finished, AtomicInteger rowCount) { + this.finished = finished; + this.rowCount = rowCount; + } + } + + private static final class RetryCounter implements TransactionRetryListener { + final CountDownLatch latch; + int retryCount = 0; + + RetryCounter() { + this(0); + } + + RetryCounter(int countDown) { + latch = new CountDownLatch(countDown); + } + + @Override + public void retryStarting(Timestamp transactionStarted, long transactionId, int retryAttempt) { + retryCount++; + latch.countDown(); + } + + @Override + public void retryFinished( + Timestamp transactionStarted, long transactionId, int retryAttempt, RetryResult result) {} + } + + private static final ExecutorService singleThreadedExecutor = Executors.newSingleThreadExecutor(); + private static final ExecutorService multiThreadedExecutor = Executors.newFixedThreadPool(8); + public static final int RANDOM_RESULT_SET_ROW_COUNT_2 = 50; + public static final Statement SELECT_RANDOM_STATEMENT_2 = Statement.of("SELECT * FROM RANDOM2"); + public static final com.google.spanner.v1.ResultSet RANDOM_RESULT_SET_2 = + new RandomResultSetGenerator(RANDOM_RESULT_SET_ROW_COUNT_2).generate(); + + @BeforeClass + public static void setupAdditionalResults() { + mockSpanner.putStatementResult( + StatementResult.query(SELECT_RANDOM_STATEMENT_2, RANDOM_RESULT_SET_2)); + } + + @AfterClass + public static void stopExecutor() { + singleThreadedExecutor.shutdown(); + multiThreadedExecutor.shutdown(); + } + + @After + public void reset() { + mockSpanner.removeAllExecutionTimes(); + } + + ITConnection createConnection(TransactionRetryListener listener) { + ITConnection connection = + super.createConnection( + ImmutableList.of(), ImmutableList.of(listener)); + connection.setAutocommit(false); + return connection; + } + + @Test + public void testSingleQueryAborted() { + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.ABORTED.asRuntimeException())); + QueryResult res = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT); + + assertThat(get(res.finished)).isNull(); + assertThat(res.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + assertThat(counter.retryCount).isEqualTo(1); + } + } + + @Test + public void testTwoQueriesSecondAborted() { + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + QueryResult res1 = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.ABORTED.asRuntimeException())); + QueryResult res2 = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT_2); + + assertThat(get(res1.finished)).isNull(); + assertThat(res1.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + assertThat(get(res2.finished)).isNull(); + assertThat(res2.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT_2); + assertThat(counter.retryCount).isEqualTo(1); + } + } + + @Test + public void testTwoQueriesBothAborted() throws InterruptedException { + RetryCounter counter = new RetryCounter(1); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.ABORTED.asRuntimeException())); + QueryResult res1 = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT); + // Wait until the first query aborted. + assertThat(counter.latch.await(10L, TimeUnit.SECONDS)).isTrue(); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.ABORTED.asRuntimeException())); + QueryResult res2 = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT_2); + + assertThat(get(res1.finished)).isNull(); + assertThat(res1.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + assertThat(get(res2.finished)).isNull(); + assertThat(res2.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT_2); + assertThat(counter.retryCount).isEqualTo(2); + } + } + + @Test + public void testSingleQueryAbortedMidway() { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofStreamException( + Status.ABORTED.asRuntimeException(), RANDOM_RESULT_SET_ROW_COUNT / 2)); + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + QueryResult res = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT); + + assertThat(get(res.finished)).isNull(); + assertThat(res.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + assertThat(counter.retryCount).isEqualTo(1); + } + } + + @Test + public void testTwoQueriesSecondAbortedMidway() { + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + QueryResult res1 = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofStreamException( + Status.ABORTED.asRuntimeException(), RANDOM_RESULT_SET_ROW_COUNT_2 / 2)); + QueryResult res2 = executeQueryAsync(connection, SELECT_RANDOM_STATEMENT_2); + + assertThat(get(res1.finished)).isNull(); + assertThat(res1.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + assertThat(get(res2.finished)).isNull(); + assertThat(res2.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT_2); + assertThat(counter.retryCount).isEqualTo(1); + } + } + + @Test + public void testTwoQueriesOneAbortedMidway() { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofStreamException( + Status.ABORTED.asRuntimeException(), + Math.min(RANDOM_RESULT_SET_ROW_COUNT / 2, RANDOM_RESULT_SET_ROW_COUNT_2 / 2))); + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + // These AsyncResultSets will be consumed in parallel. One of them will (at random) abort + // halfway. + QueryResult res1 = + executeQueryAsync(connection, SELECT_RANDOM_STATEMENT, multiThreadedExecutor); + QueryResult res2 = + executeQueryAsync(connection, SELECT_RANDOM_STATEMENT_2, multiThreadedExecutor); + + assertThat(get(res1.finished)).isNull(); + assertThat(res1.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + assertThat(get(res2.finished)).isNull(); + assertThat(res2.rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT_2); + assertThat(counter.retryCount).isEqualTo(1); + } + } + + @Test + public void testUpdateAndQueryAbortedMidway() throws InterruptedException { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofStreamException( + Status.ABORTED.asRuntimeException(), RANDOM_RESULT_SET_ROW_COUNT / 2)); + final RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + final SettableApiFuture rowCount = SettableApiFuture.create(); + final CountDownLatch updateLatch = new CountDownLatch(1); + final CountDownLatch queryLatch = new CountDownLatch(1); + ApiFuture finished; + try (AsyncResultSet rs = + connection.executeQueryAsync( + SELECT_RANDOM_STATEMENT, Options.bufferRows(RANDOM_RESULT_SET_ROW_COUNT / 2 - 1))) { + finished = + rs.setCallback( + singleThreadedExecutor, + new ReadyCallback() { + long count; + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + // Indicate that the query has been executed. + queryLatch.countDown(); + try { + // Wait until the update is on its way. + updateLatch.await(10L, TimeUnit.SECONDS); + while (true) { + switch (resultSet.tryNext()) { + case OK: + count++; + break; + case DONE: + rowCount.set(count); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + }); + } + // Wait until the query has actually executed. + queryLatch.await(10L, TimeUnit.SECONDS); + ApiFuture updateCount = connection.executeUpdateAsync(INSERT_STATEMENT); + updateCount.addListener( + new Runnable() { + @Override + public void run() { + updateLatch.countDown(); + } + }, + MoreExecutors.directExecutor()); + + // We should not commit before the AsyncResultSet has finished. + assertThat(get(finished)).isNull(); + ApiFuture commit = connection.commitAsync(); + + assertThat(get(rowCount)).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + assertThat(get(updateCount)).isEqualTo(UPDATE_COUNT); + assertThat(get(commit)).isNull(); + assertThat(counter.retryCount).isEqualTo(1); + + // Verify the order of the statements on the server. + List requests = + Lists.newArrayList( + Collections2.filter( + mockSpanner.getRequests(), + new Predicate() { + @Override + public boolean apply(AbstractMessage input) { + return input instanceof ExecuteSqlRequest; + } + })); + // The entire transaction should be retried. + assertThat(requests).hasSize(4); + assertThat(((ExecuteSqlRequest) requests.get(0)).getSeqno()).isEqualTo(1L); + assertThat(((ExecuteSqlRequest) requests.get(0)).getSql()) + .isEqualTo(SELECT_RANDOM_STATEMENT.getSql()); + assertThat(((ExecuteSqlRequest) requests.get(1)).getSeqno()).isEqualTo(2L); + assertThat(((ExecuteSqlRequest) requests.get(1)).getSql()) + .isEqualTo(INSERT_STATEMENT.getSql()); + assertThat(((ExecuteSqlRequest) requests.get(2)).getSeqno()).isEqualTo(1L); + assertThat(((ExecuteSqlRequest) requests.get(2)).getSql()) + .isEqualTo(SELECT_RANDOM_STATEMENT.getSql()); + assertThat(((ExecuteSqlRequest) requests.get(3)).getSeqno()).isEqualTo(2L); + assertThat(((ExecuteSqlRequest) requests.get(3)).getSql()) + .isEqualTo(INSERT_STATEMENT.getSql()); + } + } + + @Test + public void testUpdateAndQueryAbortedMidway_UpdateCountChanged() throws InterruptedException { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofStreamException( + Status.ABORTED.asRuntimeException(), RANDOM_RESULT_SET_ROW_COUNT / 2)); + final RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + assertThat(counter.retryCount).isEqualTo(0); + final CountDownLatch updateLatch = new CountDownLatch(1); + final CountDownLatch queryLatch = new CountDownLatch(1); + ApiFuture finished; + try (AsyncResultSet rs = + connection.executeQueryAsync( + SELECT_RANDOM_STATEMENT, Options.bufferRows(RANDOM_RESULT_SET_ROW_COUNT / 2 - 1))) { + finished = + rs.setCallback( + singleThreadedExecutor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + // Indicate that the query has been executed. + queryLatch.countDown(); + try { + // Wait until the update is on its way. + updateLatch.await(10L, TimeUnit.SECONDS); + while (true) { + switch (resultSet.tryNext()) { + case OK: + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + }); + } + // Wait until the query has actually executed. + queryLatch.await(10L, TimeUnit.SECONDS); + // Execute an update statement and wait until it has finished before allowing the + // AsyncResultSet to continue processing. Also change the result of the update statement after + // it has finished. The AsyncResultSet will see an aborted transaction halfway, and then + // during the retry, it will get a different result for this update statement. That will cause + // the retry to be aborted. + get(connection.executeUpdateAsync(INSERT_STATEMENT)); + try { + mockSpanner.putStatementResult(StatementResult.update(INSERT_STATEMENT, UPDATE_COUNT + 1)); + updateLatch.countDown(); + get(finished); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertThat(counter.retryCount).isEqualTo(1); + } finally { + mockSpanner.putStatementResult(StatementResult.update(INSERT_STATEMENT, UPDATE_COUNT)); + } + + // Verify the order of the statements on the server. + List requests = + Lists.newArrayList( + Collections2.filter( + mockSpanner.getRequests(), + new Predicate() { + @Override + public boolean apply(AbstractMessage input) { + return input instanceof ExecuteSqlRequest; + } + })); + // The entire transaction should be retried, but will not succeed as the result of the update + // statement was different during the retry. + assertThat(requests).hasSize(4); + assertThat(((ExecuteSqlRequest) requests.get(0)).getSeqno()).isEqualTo(1L); + assertThat(((ExecuteSqlRequest) requests.get(0)).getSql()) + .isEqualTo(SELECT_RANDOM_STATEMENT.getSql()); + assertThat(((ExecuteSqlRequest) requests.get(1)).getSeqno()).isEqualTo(2L); + assertThat(((ExecuteSqlRequest) requests.get(1)).getSql()) + .isEqualTo(INSERT_STATEMENT.getSql()); + assertThat(((ExecuteSqlRequest) requests.get(2)).getSeqno()).isEqualTo(1L); + assertThat(((ExecuteSqlRequest) requests.get(2)).getSql()) + .isEqualTo(SELECT_RANDOM_STATEMENT.getSql()); + assertThat(((ExecuteSqlRequest) requests.get(3)).getSeqno()).isEqualTo(2L); + assertThat(((ExecuteSqlRequest) requests.get(3)).getSql()) + .isEqualTo(INSERT_STATEMENT.getSql()); + } + } + + @Test + public void testQueriesAbortedMidway_ResultsChanged() throws InterruptedException { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofStreamException( + Status.ABORTED.asRuntimeException(), RANDOM_RESULT_SET_ROW_COUNT - 1)); + final Statement statement = Statement.of("SELECT * FROM TEST_TABLE"); + final RandomResultSetGenerator generator = + new RandomResultSetGenerator(RANDOM_RESULT_SET_ROW_COUNT - 10); + mockSpanner.putStatementResult(StatementResult.query(statement, generator.generate())); + + final CountDownLatch latch = new CountDownLatch(1); + final RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + ApiFuture res1; + try (AsyncResultSet rs = + connection.executeQueryAsync(SELECT_RANDOM_STATEMENT, Options.bufferRows(5))) { + res1 = + rs.setCallback( + multiThreadedExecutor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + latch.await(10L, TimeUnit.SECONDS); + while (true) { + switch (resultSet.tryNext()) { + case OK: + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } catch (Throwable t) { + throw SpannerExceptionFactory.asSpannerException(t); + } + } + }); + } + try (AsyncResultSet rs = connection.executeQueryAsync(statement, Options.bufferRows(5))) { + rs.setCallback( + multiThreadedExecutor, + new ReadyCallback() { + boolean replaced; + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + if (!replaced) { + // Replace the result of the query on the server after the first execution. + mockSpanner.putStatementResult( + StatementResult.query(statement, generator.generate())); + replaced = true; + } + while (true) { + switch (resultSet.tryNext()) { + case OK: + break; + case DONE: + latch.countDown(); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } + }); + } + try { + get(res1); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertThat(counter.retryCount).isEqualTo(1); + } + } + } + + @Test + public void testBlindUpdateAborted() { + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + mockSpanner.abortNextStatement(); + ApiFuture updateCount = connection.executeUpdateAsync(INSERT_STATEMENT); + get(connection.commitAsync()); + + assertThat(get(updateCount)).isEqualTo(UPDATE_COUNT); + assertThat(counter.retryCount).isEqualTo(1); + } + } + + @Test + public void testBlindUpdateAborted_WithConcurrentModification() { + Statement update1 = Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=100"); + mockSpanner.putStatementResult(StatementResult.update(update1, 100)); + + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + // Execute an update statement and then change the result for the next time it is executed. + get(connection.executeUpdateAsync(update1)); + mockSpanner.putStatementResult(StatementResult.update(update1, 200)); + + // Abort on the next statement. The retry should now fail because of the changed result of the + // first update. + mockSpanner.abortNextStatement(); + connection.executeUpdateAsync(INSERT_STATEMENT); + + try { + get(connection.commitAsync()); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertThat(counter.retryCount).isEqualTo(1); + } + } + } + + @Test + public void testMultipleBlindUpdatesAborted_WithConcurrentModification() { + Statement update1 = Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=100"); + mockSpanner.putStatementResult(StatementResult.update(update1, 100)); + + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + // Execute an update statement and then change the result for the next time it is executed. + get(connection.executeUpdateAsync(update1)); + mockSpanner.putStatementResult(StatementResult.update(update1, 200)); + + // Abort the transaction on the next statement. The retry should now fail because of the + // changed result of the first update. + mockSpanner.abortNextStatement(); + + // Continue to (try to) execute blind updates. This should not cause any exceptions, although + // all of the returned futures will fail. + List> futures = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + futures.add(connection.executeUpdateAsync(INSERT_STATEMENT)); + } + + for (ApiFuture fut : futures) { + try { + get(fut); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertThat(counter.retryCount).isEqualTo(1); + } + } + } + } + + @Test + public void testBlindUpdateAborted_ThenAsyncQuery_WithConcurrentModification() { + Statement update1 = Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=100"); + mockSpanner.putStatementResult(StatementResult.update(update1, 100)); + + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + // Execute an update statement and then change the result for the next time it is executed. + get(connection.executeUpdateAsync(update1)); + mockSpanner.putStatementResult(StatementResult.update(update1, 200)); + + // Abort on the next statement. The retry should now fail because of the changed result of the + // first update. + mockSpanner.abortNextStatement(); + connection.executeUpdateAsync(INSERT_STATEMENT); + + // Try to execute an async query. The callback should also receive the + // AbortedDueToConcurrentModificationException. + try (AsyncResultSet rs = connection.executeQueryAsync(SELECT_RANDOM_STATEMENT)) { + ApiFuture fut = + rs.setCallback( + singleThreadedExecutor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + // The following line should throw AbortedDueToConcurrentModificationException. + resultSet.tryNext(); + return CallbackResponse.DONE; + } + }); + try { + assertThat(get(fut)).isNull(); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertThat(counter.retryCount).isEqualTo(1); + } + } + + // Ensure that a rollback and then a new statement does succeed. + connection.rollbackAsync(); + try (AsyncResultSet rs = connection.executeQueryAsync(SELECT_RANDOM_STATEMENT)) { + ApiFuture fut = + rs.setCallback( + singleThreadedExecutor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + resultSet.tryNext(); + return CallbackResponse.DONE; + } + }); + assertThat(get(fut)).isNull(); + } + get(connection.commitAsync()); + } + } + + @Test + public void testBlindUpdateAborted_SelectResults() { + final Statement update1 = Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=100"); + mockSpanner.putStatementResult(StatementResult.update(update1, 100)); + + RetryCounter counter = new RetryCounter(); + try (Connection connection = createConnection(counter)) { + // Execute an update statement and then change the result for the next time it is executed. + connection.executeUpdate(update1); + // Abort on the next statement. The retry should now fail because of the changed result of the + // first update. + mockSpanner.abortNextStatement(); + mockSpanner.putStatementResult(StatementResult.update(update1, 200)); + connection.executeUpdateAsync(INSERT_STATEMENT); + ApiFuture commit = connection.commitAsync(); + + try (AsyncResultSet rs = connection.executeQueryAsync(SELECT_RANDOM_STATEMENT)) { + while (rs.next()) {} + } + get(connection.commitAsync()); + + try { + get(commit); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertThat(counter.retryCount).isEqualTo(1); + } + } + } + + private QueryResult executeQueryAsync(Connection connection, Statement statement) { + return executeQueryAsync(connection, statement, singleThreadedExecutor); + } + + private QueryResult executeQueryAsync( + Connection connection, Statement statement, Executor executor) { + ApiFuture res; + final AtomicInteger rowCount = new AtomicInteger(); + try (AsyncResultSet rs = connection.executeQueryAsync(statement, Options.bufferRows(5))) { + res = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case OK: + rowCount.incrementAndGet(); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } + }); + return new QueryResult(res, rowCount); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiTest.java new file mode 100644 index 00000000000..39d33ae1cae --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionAsyncApiTest.java @@ -0,0 +1,833 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.SpannerApiFutures.get; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerApiFutures; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.connection.StatementResult.ResultType; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.protobuf.AbstractMessage; +import com.google.spanner.v1.ExecuteBatchDmlRequest; +import com.google.spanner.v1.ExecuteSqlRequest; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ConnectionAsyncApiTest extends AbstractMockServerTest { + private static final ExecutorService executor = Executors.newSingleThreadExecutor(); + private static final Function AUTOCOMMIT = + new Function() { + @Override + public Void apply(Connection input) { + input.setAutocommit(true); + return null; + } + }; + private static final Function READ_ONLY = + new Function() { + @Override + public Void apply(Connection input) { + input.setReadOnly(true); + return null; + } + }; + private static final Function READ_WRITE = + new Function() { + @Override + public Void apply(Connection input) { + return null; + } + }; + + @AfterClass + public static void stopExecutor() { + executor.shutdown(); + } + + @After + public void reset() { + mockSpanner.removeAllExecutionTimes(); + } + + @Test + public void testExecuteQueryAsyncAutocommit() { + testExecuteQueryAsync(AUTOCOMMIT); + } + + @Test + public void testExecuteQueryAsyncAutocommitIsNonBlocking() { + testExecuteQueryAsyncIsNonBlocking(AUTOCOMMIT); + } + + @Test + public void testExecuteQueryAsStatementAsyncAutocommit() { + testExecuteQueryAsync(AUTOCOMMIT, true); + } + + @Test + public void testExecuteQueryAutocommit() { + testExecuteQuery(AUTOCOMMIT); + } + + @Test + public void testExecuteUpdateAsyncAutocommit() { + testExecuteUpdateAsync(AUTOCOMMIT); + } + + @Test + public void testExecuteUpdateAsyncAutocommitIsNonBlocking() { + testExecuteUpdateAsyncIsNonBlocking(AUTOCOMMIT); + } + + @Test + public void testExecuteUpdateAsStatementAsyncAutocommit() { + testExecuteUpdateAsync(AUTOCOMMIT, true); + } + + @Test + public void testExecuteUpdateAutocommit() { + testExecuteUpdate(AUTOCOMMIT); + } + + @Test + public void testExecuteBatchUpdateAsyncAutocommit() { + testExecuteBatchUpdateAsync(AUTOCOMMIT); + } + + @Test + public void testExecuteBatchUpdateAsyncAutocommitIsNonBlocking() { + testExecuteBatchUpdateAsyncIsNonBlocking(AUTOCOMMIT); + } + + @Test + public void testExecuteBatchUpdateAutocommit() { + testExecuteBatchUpdate(AUTOCOMMIT); + } + + @Test + public void testWriteAsyncAutocommit() { + testWriteAsync(AUTOCOMMIT); + } + + @Test + public void testWriteAutocommit() { + testWrite(AUTOCOMMIT); + } + + @Test + public void testExecuteQueryAsyncReadOnly() { + testExecuteQueryAsync(READ_ONLY); + } + + @Test + public void testExecuteQueryAsyncReadOnlyIsNonBlocking() { + testExecuteQueryAsyncIsNonBlocking(READ_ONLY); + } + + @Test + public void testExecuteQueryAsStatementAsyncReadOnly() { + testExecuteQueryAsync(READ_ONLY, true); + } + + @Test + public void testExecuteQueryReadOnly() { + testExecuteQuery(READ_ONLY); + } + + @Test + public void testExecuteQueryAsyncReadWrite() { + testExecuteQueryAsync(READ_WRITE); + } + + @Test + public void testExecuteQueryAsyncReadWriteIsNonBlocking() { + testExecuteQueryAsyncIsNonBlocking(READ_WRITE); + } + + @Test + public void testExecuteQueryAsStatementAsyncReadWrite() { + testExecuteQueryAsync(READ_WRITE, true); + } + + @Test + public void testExecuteQueryReadWrite() { + testExecuteQuery(READ_WRITE); + } + + @Test + public void testExecuteUpdateAsyncReadWrite() { + testExecuteUpdateAsync(READ_WRITE); + } + + @Test + public void testExecuteUpdateAsyncReadWriteIsNonBlocking() { + testExecuteUpdateAsyncIsNonBlocking(READ_WRITE); + } + + @Test + public void testExecuteUpdateAsStatementAsyncReadWrite() { + testExecuteUpdateAsync(READ_WRITE, true); + } + + @Test + public void testExecuteUpdateReadWrite() { + testExecuteUpdate(READ_WRITE); + } + + @Test + public void testExecuteBatchUpdateAsyncReadWrite() { + testExecuteBatchUpdateAsync(READ_WRITE); + } + + @Test + public void testExecuteBatchUpdateAsyncReadWriteIsNonBlocking() { + testExecuteBatchUpdateAsyncIsNonBlocking(READ_WRITE); + } + + @Test + public void testExecuteBatchUpdateReadWrite() { + testExecuteBatchUpdate(READ_WRITE); + } + + @Test + public void testBufferedWriteReadWrite() { + testBufferedWrite(READ_WRITE); + } + + @Test + public void testReadWriteMultipleAsyncStatements() { + try (Connection connection = createConnection()) { + assertThat(connection.isAutocommit()).isFalse(); + ApiFuture update1 = connection.executeUpdateAsync(INSERT_STATEMENT); + ApiFuture update2 = connection.executeUpdateAsync(INSERT_STATEMENT); + ApiFuture batch = + connection.executeBatchUpdateAsync(ImmutableList.of(INSERT_STATEMENT, INSERT_STATEMENT)); + final SettableApiFuture rowCount = SettableApiFuture.create(); + try (AsyncResultSet rs = connection.executeQueryAsync(SELECT_RANDOM_STATEMENT)) { + rs.setCallback( + executor, + new ReadyCallback() { + int count = 0; + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + rowCount.set(count); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + count++; + } + } + } catch (SpannerException e) { + rowCount.setException(e); + return CallbackResponse.DONE; + } + } + }); + } + connection.commitAsync(); + assertThat(get(update1)).isEqualTo(UPDATE_COUNT); + assertThat(get(update2)).isEqualTo(UPDATE_COUNT); + assertThat(get(batch)).asList().containsExactly(1L, 1L); + assertThat(get(rowCount)).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + + // Verify the order of the statements on the server. + List requests = + Lists.newArrayList( + Collections2.filter( + mockSpanner.getRequests(), + new Predicate() { + @Override + public boolean apply(AbstractMessage input) { + return input instanceof ExecuteSqlRequest + || input instanceof ExecuteBatchDmlRequest; + } + })); + assertThat(requests).hasSize(4); + assertThat(requests.get(0)).isInstanceOf(ExecuteSqlRequest.class); + assertThat(((ExecuteSqlRequest) requests.get(0)).getSeqno()).isEqualTo(1L); + assertThat(requests.get(1)).isInstanceOf(ExecuteSqlRequest.class); + assertThat(((ExecuteSqlRequest) requests.get(1)).getSeqno()).isEqualTo(2L); + assertThat(requests.get(2)).isInstanceOf(ExecuteBatchDmlRequest.class); + assertThat(((ExecuteBatchDmlRequest) requests.get(2)).getSeqno()).isEqualTo(3L); + assertThat(requests.get(3)).isInstanceOf(ExecuteSqlRequest.class); + assertThat(((ExecuteSqlRequest) requests.get(3)).getSeqno()).isEqualTo(4L); + } + } + + @Test + public void testAutocommitRunBatch() { + try (Connection connection = createConnection()) { + connection.setAutocommit(true); + connection.execute(Statement.of("START BATCH DML")); + connection.execute(INSERT_STATEMENT); + connection.execute(INSERT_STATEMENT); + StatementResult res = connection.execute(Statement.of("RUN BATCH")); + assertThat(res.getResultType()).isEqualTo(ResultType.RESULT_SET); + try (ResultSet rs = res.getResultSet()) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLongList(0)).containsExactly(1L, 1L); + assertThat(rs.next()).isFalse(); + } + } + } + + @Test + public void testAutocommitRunBatchAsync() { + try (Connection connection = createConnection()) { + connection.executeAsync(Statement.of("SET AUTOCOMMIT = TRUE")); + connection.executeAsync(Statement.of("START BATCH DML")); + connection.executeAsync(INSERT_STATEMENT); + connection.executeAsync(INSERT_STATEMENT); + ApiFuture res = connection.runBatchAsync(); + assertThat(get(res)).asList().containsExactly(1L, 1L); + } + } + + @Test + public void testExecuteDdlAsync() { + try (Connection connection = createConnection()) { + connection.executeAsync(Statement.of("SET AUTOCOMMIT = TRUE")); + connection.executeAsync(Statement.of("START BATCH DDL")); + connection.executeAsync(Statement.of("CREATE TABLE FOO (ID INT64) PRIMARY KEY (ID)")); + connection.executeAsync(Statement.of("ABORT BATCH")); + } + } + + @Test + public void testExecuteInvalidStatementAsync() { + try (Connection connection = createConnection()) { + try { + connection.executeAsync(Statement.of("UPSERT INTO FOO (ID, VAL) VALUES (1, 'foo')")); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } + } + + @Test + public void testExecuteClientSideQueryAsync() { + try (Connection connection = createConnection()) { + connection.executeAsync(Statement.of("SET AUTOCOMMIT = TRUE")); + final SettableApiFuture autocommit = SettableApiFuture.create(); + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SHOW VARIABLE AUTOCOMMIT"))) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + autocommit.set(resultSet.getBoolean("AUTOCOMMIT")); + } + } + } + }); + } + assertThat(get(autocommit)).isTrue(); + } + } + + @Test + public void testExecuteInvalidQueryAsync() { + try (Connection connection = createConnection()) { + try { + connection.executeQueryAsync(INSERT_STATEMENT); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } + } + + @Test + public void testExecuteInvalidUpdateAsync() { + try (Connection connection = createConnection()) { + try { + connection.executeUpdateAsync(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } + } + + @Test + public void testExecuteInvalidBatchUpdateAsync() { + try (Connection connection = createConnection()) { + try { + connection.executeBatchUpdateAsync( + ImmutableList.of(INSERT_STATEMENT, SELECT_RANDOM_STATEMENT)); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } + } + + @Test + public void testRunEmptyBatchAsync() { + try (Connection connection = createConnection()) { + connection.startBatchDml(); + assertThat(get(connection.runBatchAsync())).isEqualTo(new long[0]); + } + } + + private void testExecuteQueryAsync(Function connectionConfigurator) { + testExecuteQueryAsync(connectionConfigurator, false); + } + + private void testExecuteQueryAsync( + Function connectionConfigurator, boolean executeAsStatement) { + ApiFuture res; + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + final AtomicInteger rowCount = new AtomicInteger(); + final AtomicBoolean receivedTimeout = new AtomicBoolean(); + if (timeout) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + try (AsyncResultSet rs = + executeAsStatement + ? connection.executeAsync(SELECT_RANDOM_STATEMENT).getResultSetAsync() + : connection.executeQueryAsync(SELECT_RANDOM_STATEMENT)) { + res = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case OK: + rowCount.incrementAndGet(); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } catch (SpannerException e) { + receivedTimeout.set(e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED); + throw e; + } + } + }); + } + try { + SpannerApiFutures.get(res); + assertThat(rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + if (connection.isReadOnly() || !connection.isInTransaction()) { + assertThat(connection.getReadTimestamp()).isNotNull(); + } + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(e.getSuppressed()).hasLength(1); + assertThat(e.getSuppressed()[0].getMessage()).contains(SELECT_RANDOM_STATEMENT.getSql()); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + assertThat(timeout).isTrue(); + assertThat(receivedTimeout.get()).isTrue(); + // Start a new transaction if a timeout occurred on a read/write transaction, as that will + // invalidate that transaction. + if (!connection.isReadOnly() && connection.isInTransaction()) { + connection.clearStatementTimeout(); + connection.rollback(); + } + } + } + } + } + + private void testExecuteQuery(Function connectionConfigurator) { + long rowCount = 0L; + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + while (rs.next()) { + rowCount++; + } + assertThat(rowCount).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + if (connection.isReadOnly() || !connection.isInTransaction()) { + assertThat(connection.getReadTimestamp()).isNotNull(); + } + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(timeout).isTrue(); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + // Start a new transaction if a timeout occurred on a read/write transaction, as that will + // invalidate that transaction. + if (!connection.isReadOnly() && connection.isInTransaction()) { + connection.clearStatementTimeout(); + connection.rollback(); + } + } + } + } + } + + private void testExecuteUpdateAsync(Function connectionConfigurator) { + testExecuteUpdateAsync(connectionConfigurator, false); + } + + private void testExecuteUpdateAsync( + Function connectionConfigurator, boolean executeAsStatement) { + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setExecuteSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + ApiFuture updateCount = + executeAsStatement + ? connection.executeAsync(INSERT_STATEMENT).getUpdateCountAsync() + : connection.executeUpdateAsync(INSERT_STATEMENT); + try { + assertThat(get(updateCount)).isEqualTo(1L); + if (connection.isInTransaction()) { + connection.commitAsync(); + } + assertThat(connection.getCommitTimestamp()).isNotNull(); + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(timeout).isTrue(); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + // Start a new transaction if a timeout occurred on a read/write transaction, as that will + // invalidate that transaction. + if (!connection.isReadOnly() && connection.isInTransaction()) { + connection.clearStatementTimeout(); + connection.rollback(); + } + } + } + } + } + + private void testExecuteUpdate(Function connectionConfigurator) { + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setExecuteSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + try { + long updateCount = connection.executeUpdate(INSERT_STATEMENT); + assertThat(updateCount).isEqualTo(1L); + if (connection.isInTransaction()) { + connection.commit(); + } + assertThat(connection.getCommitTimestamp()).isNotNull(); + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(timeout).isTrue(); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + // Start a new transaction if a timeout occurred on a read/write transaction, as that will + // invalidate that transaction. + if (!connection.isReadOnly() && connection.isInTransaction()) { + connection.clearStatementTimeout(); + connection.rollback(); + } + } + } + } + } + + private void testExecuteBatchUpdateAsync(Function connectionConfigurator) { + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setExecuteBatchDmlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + ApiFuture updateCounts = + connection.executeBatchUpdateAsync( + ImmutableList.of(INSERT_STATEMENT, INSERT_STATEMENT)); + try { + assertThat(get(updateCounts)).asList().containsExactly(1L, 1L); + if (connection.isInTransaction()) { + connection.commitAsync(); + } + assertThat(connection.getCommitTimestamp()).isNotNull(); + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(timeout).isTrue(); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + // Start a new transaction if a timeout occurred on a read/write transaction, as that will + // invalidate that transaction. + if (!connection.isReadOnly() && connection.isInTransaction()) { + connection.clearStatementTimeout(); + connection.rollback(); + } + } + } + } + } + + private void testExecuteBatchUpdate(Function connectionConfigurator) { + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setExecuteBatchDmlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + try { + long[] updateCounts = + connection.executeBatchUpdate(ImmutableList.of(INSERT_STATEMENT, INSERT_STATEMENT)); + assertThat(updateCounts).asList().containsExactly(1L, 1L); + if (connection.isInTransaction()) { + connection.commit(); + } + assertThat(connection.getCommitTimestamp()).isNotNull(); + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + assertThat(timeout).isTrue(); + // Start a new transaction if a timeout occurred on a read/write transaction, as that will + // invalidate that transaction. + if (!connection.isReadOnly() && connection.isInTransaction()) { + connection.clearStatementTimeout(); + connection.rollback(); + } + } + } + } + } + + private void testWriteAsync(Function connectionConfigurator) { + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setCommitExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + ApiFuture fut = + connection.writeAsync( + ImmutableList.of( + Mutation.newInsertBuilder("foo").build(), + Mutation.newInsertBuilder("bar").build())); + try { + assertThat(get(fut)).isNull(); + assertThat(connection.getCommitTimestamp()).isNotNull(); + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(timeout).isTrue(); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + } + } + } + } + + private void testWrite(Function connectionConfigurator) { + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setCommitExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + try { + connection.write( + ImmutableList.of( + Mutation.newInsertBuilder("foo").build(), + Mutation.newInsertBuilder("bar").build())); + assertThat(connection.getCommitTimestamp()).isNotNull(); + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(timeout).isTrue(); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + } + } + } + } + + private void testBufferedWrite(Function connectionConfigurator) { + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + for (boolean timeout : new boolean[] {true, false}) { + if (timeout) { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(1000, 0)); + connection.setStatementTimeout(1L, TimeUnit.NANOSECONDS); + } else { + mockSpanner.removeAllExecutionTimes(); + connection.clearStatementTimeout(); + } + try { + connection.bufferedWrite( + ImmutableList.of( + Mutation.newInsertBuilder("foo").build(), + Mutation.newInsertBuilder("bar").build())); + connection.commitAsync(); + assertThat(connection.getCommitTimestamp()).isNotNull(); + assertThat(timeout).isFalse(); + } catch (SpannerException e) { + assertThat(timeout).isTrue(); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + connection.clearStatementTimeout(); + connection.rollbackAsync(); + } + } + } + } + + private void testExecuteQueryAsyncIsNonBlocking( + Function connectionConfigurator) { + ApiFuture res; + final AtomicInteger rowCount = new AtomicInteger(); + mockSpanner.freeze(); + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + try (AsyncResultSet rs = connection.executeQueryAsync(SELECT_RANDOM_STATEMENT)) { + res = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case OK: + rowCount.incrementAndGet(); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } + }); + mockSpanner.unfreeze(); + } + SpannerApiFutures.get(res); + assertThat(rowCount.get()).isEqualTo(RANDOM_RESULT_SET_ROW_COUNT); + } + } + + private void testExecuteUpdateAsyncIsNonBlocking( + Function connectionConfigurator) { + mockSpanner.freeze(); + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + ApiFuture updateCount = connection.executeUpdateAsync(INSERT_STATEMENT); + if (connection.isInTransaction()) { + connection.commitAsync(); + } + mockSpanner.unfreeze(); + assertThat(get(updateCount)).isEqualTo(1L); + assertThat(connection.getCommitTimestamp()).isNotNull(); + } + } + + private void testExecuteBatchUpdateAsyncIsNonBlocking( + Function connectionConfigurator) { + mockSpanner.freeze(); + try (Connection connection = createConnection()) { + connectionConfigurator.apply(connection); + ApiFuture updateCounts = + connection.executeBatchUpdateAsync(ImmutableList.of(INSERT_STATEMENT, INSERT_STATEMENT)); + if (connection.isInTransaction()) { + connection.commitAsync(); + } + mockSpanner.unfreeze(); + assertThat(get(updateCounts)).asList().containsExactly(1L, 1L); + assertThat(connection.getCommitTimestamp()).isNotNull(); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java index f5295ef96b9..88f942122a9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java @@ -53,14 +53,13 @@ import com.google.cloud.spanner.TimestampBound.Mode; import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.cloud.spanner.TransactionRunner; -import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.connection.AbstractConnectionImplTest.ConnectionConsumer; import com.google.cloud.spanner.connection.ConnectionImpl.UnitOfWorkType; import com.google.cloud.spanner.connection.ConnectionStatementExecutorImpl.StatementTimeoutGetter; import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.GetExactStaleness; +import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.cloud.spanner.connection.StatementResult.ResultType; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; @@ -74,6 +73,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Matchers; +import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -319,11 +319,16 @@ public TransactionRunner answer(InvocationOnMock invocation) { new TransactionRunner() { private Timestamp commitTimestamp; - @SuppressWarnings("unchecked") @Override public T run(TransactionCallable callable) { this.commitTimestamp = Timestamp.now(); - return (T) Long.valueOf(1L); + TransactionContext tx = mock(TransactionContext.class); + when(tx.executeUpdate(Statement.of(UPDATE))).thenReturn(1L); + try { + return callable.run(tx); + } catch (Exception e) { + throw SpannerExceptionFactory.newSpannerException(e); + } } @Override @@ -1199,6 +1204,9 @@ public void testMergeQueryOptions() { DdlClient ddlClient = mock(DdlClient.class); DatabaseClient dbClient = mock(DatabaseClient.class); final UnitOfWork unitOfWork = mock(UnitOfWork.class); + when(unitOfWork.executeQueryAsync( + any(ParsedStatement.class), any(AnalyzeMode.class), Mockito.anyVararg())) + .thenReturn(ApiFutures.immediateFuture(mock(ResultSet.class))); try (ConnectionImpl impl = new ConnectionImpl(connectionOptions, spannerPool, ddlClient, dbClient) { @Override @@ -1210,7 +1218,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() { impl.setOptimizerVersion("1"); impl.executeQuery(Statement.of("SELECT FOO FROM BAR")); verify(unitOfWork) - .executeQuery( + .executeQueryAsync( StatementParser.INSTANCE.parse( Statement.newBuilder("SELECT FOO FROM BAR") .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("1").build()) @@ -1221,7 +1229,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() { impl.setOptimizerVersion("2"); impl.executeQuery(Statement.of("SELECT FOO FROM BAR")); verify(unitOfWork) - .executeQuery( + .executeQueryAsync( StatementParser.INSTANCE.parse( Statement.newBuilder("SELECT FOO FROM BAR") .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("2").build()) @@ -1234,7 +1242,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() { impl.setOptimizerVersion("3"); impl.executeQuery(Statement.of("SELECT FOO FROM BAR"), prefetchOption); verify(unitOfWork) - .executeQuery( + .executeQueryAsync( StatementParser.INSTANCE.parse( Statement.newBuilder("SELECT FOO FROM BAR") .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("3").build()) @@ -1251,7 +1259,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() { .build(), prefetchOption); verify(unitOfWork) - .executeQuery( + .executeQueryAsync( StatementParser.INSTANCE.parse( Statement.newBuilder("SELECT FOO FROM BAR") .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("5").build()) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionTest.java index 5838e7778d2..de820ccbcc5 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionTest.java @@ -17,10 +17,15 @@ package com.google.cloud.spanner.connection; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import com.google.cloud.spanner.AbortedException; +import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; +import com.google.common.collect.ImmutableList; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import org.junit.Test; @@ -94,4 +99,76 @@ public String getOptimizerVersion() { SpannerOptions.useDefaultEnvironment(); } } + + @Test + public void testExecuteInvalidBatchUpdate() { + try (Connection connection = createConnection()) { + try { + connection.executeBatchUpdate(ImmutableList.of(INSERT_STATEMENT, SELECT_RANDOM_STATEMENT)); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } + } + + @Test + public void testQueryAborted() { + try (Connection connection = createConnection()) { + connection.setRetryAbortsInternally(false); + for (boolean abort : new Boolean[] {true, false}) { + try { + if (abort) { + mockSpanner.abortNextStatement(); + } + connection.executeQuery(SELECT_RANDOM_STATEMENT); + assertThat(abort).isFalse(); + connection.commit(); + } catch (AbortedException e) { + assertThat(abort).isTrue(); + connection.rollback(); + } + } + } + } + + @Test + public void testUpdateAborted() { + try (Connection connection = createConnection()) { + connection.setRetryAbortsInternally(false); + for (boolean abort : new Boolean[] {true, false}) { + try { + if (abort) { + mockSpanner.abortNextStatement(); + } + connection.executeUpdate(INSERT_STATEMENT); + assertThat(abort).isFalse(); + connection.commit(); + } catch (AbortedException e) { + assertThat(abort).isTrue(); + connection.rollback(); + } + } + } + } + + @Test + public void testBatchUpdateAborted() { + try (Connection connection = createConnection()) { + connection.setRetryAbortsInternally(false); + for (boolean abort : new Boolean[] {true, false}) { + try { + if (abort) { + mockSpanner.abortNextStatement(); + } + connection.executeBatchUpdate(ImmutableList.of(INSERT_STATEMENT, INSERT_STATEMENT)); + assertThat(abort).isFalse(); + connection.commit(); + } catch (AbortedException e) { + assertThat(abort).isTrue(); + connection.rollback(); + } + } + } + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java index 6194b7f73b3..1e09eb70f50 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -50,6 +51,7 @@ import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import io.grpc.Status; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -137,7 +139,7 @@ private DdlBatch createSubject(DdlClient ddlClient, DatabaseClient dbClient) { public void testExecuteQuery() { DdlBatch batch = createSubject(); try { - batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); + batch.executeQueryAsync(mock(ParsedStatement.class), AnalyzeMode.NONE); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -156,16 +158,18 @@ public void testExecuteMetadataQuery() { when(singleUse.executeQuery(statement)).thenReturn(resultSet); when(dbClient.singleUse()).thenReturn(singleUse); DdlBatch batch = createSubject(createDefaultMockDdlClient(), dbClient); - ResultSet result = - batch.executeQuery(parsedStatement, AnalyzeMode.NONE, InternalMetadataQuery.INSTANCE); - assertThat(result.hashCode(), is(equalTo(resultSet.hashCode()))); + assertThat( + get(batch.executeQueryAsync( + parsedStatement, AnalyzeMode.NONE, InternalMetadataQuery.INSTANCE)) + .hashCode(), + is(equalTo(resultSet.hashCode()))); } @Test public void testExecuteUpdate() { DdlBatch batch = createSubject(); try { - batch.executeUpdate(mock(ParsedStatement.class)); + batch.executeUpdateAsync(mock(ParsedStatement.class)); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -173,11 +177,10 @@ public void testExecuteUpdate() { } @Test - public void testGetCommitTimestamp() { + public void testExecuteBatchUpdate() { DdlBatch batch = createSubject(); - batch.runBatch(); try { - batch.getCommitTimestamp(); + batch.executeBatchUpdateAsync(Collections.singleton(mock(ParsedStatement.class))); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -185,11 +188,11 @@ public void testGetCommitTimestamp() { } @Test - public void testGetReadTimestamp() { + public void testGetCommitTimestamp() { DdlBatch batch = createSubject(); - batch.runBatch(); + get(batch.runBatchAsync()); try { - batch.getReadTimestamp(); + batch.getCommitTimestamp(); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -197,10 +200,11 @@ public void testGetReadTimestamp() { } @Test - public void testWrite() { + public void testGetReadTimestamp() { DdlBatch batch = createSubject(); + get(batch.runBatchAsync()); try { - batch.write(Mutation.newInsertBuilder("foo").build()); + batch.getReadTimestamp(); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -211,7 +215,7 @@ public void testWrite() { public void testWriteIterable() { DdlBatch batch = createSubject(); try { - batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); + batch.writeAsync(Arrays.asList(Mutation.newInsertBuilder("foo").build())); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -229,7 +233,7 @@ public void testGetStateAndIsActive() { DdlBatch batch = createSubject(); assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); assertThat(batch.isActive(), is(true)); - batch.runBatch(); + get(batch.runBatchAsync()); assertThat(batch.getState(), is(UnitOfWorkState.RAN)); assertThat(batch.isActive(), is(false)); @@ -241,7 +245,9 @@ public void testGetStateAndIsActive() { assertThat(batch.isActive(), is(false)); DdlClient client = mock(DdlClient.class); - doThrow(SpannerException.class).when(client).executeDdl(anyListOf(String.class)); + SpannerException exception = mock(SpannerException.class); + when(exception.getErrorCode()).thenReturn(ErrorCode.FAILED_PRECONDITION); + doThrow(exception).when(client).executeDdl(anyListOf(String.class)); batch = createSubject(client); assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); assertThat(batch.isActive(), is(true)); @@ -249,14 +255,13 @@ public void testGetStateAndIsActive() { when(statement.getStatement()).thenReturn(Statement.of("CREATE TABLE FOO")); when(statement.getSqlWithoutComments()).thenReturn("CREATE TABLE FOO"); when(statement.getType()).thenReturn(StatementType.DDL); - batch.executeDdl(statement); - boolean exception = false; + batch.executeDdlAsync(statement); try { - batch.runBatch(); + get(batch.runBatchAsync()); + fail("Missing expected exception"); } catch (SpannerException e) { - exception = true; + assertThat(e.getErrorCode(), is(equalTo(ErrorCode.FAILED_PRECONDITION))); } - assertThat(exception, is(true)); assertThat(batch.getState(), is(UnitOfWorkState.RUN_FAILED)); assertThat(batch.isActive(), is(false)); } @@ -287,7 +292,7 @@ public boolean matches(Object list) { public void testRunBatch() { DdlClient client = createDefaultMockDdlClient(); DdlBatch batch = createSubject(client); - batch.runBatch(); + get(batch.runBatchAsync()); assertThat(batch.getState(), is(UnitOfWorkState.RAN)); verify(client, never()).executeDdl(anyString()); verify(client, never()).executeDdl(argThat(isEmptyListOfStrings())); @@ -299,20 +304,20 @@ public void testRunBatch() { client = createDefaultMockDdlClient(); batch = createSubject(client); - batch.executeDdl(statement); - batch.runBatch(); + batch.executeDdlAsync(statement); + get(batch.runBatchAsync()); verify(client).executeDdl(argThat(isListOfStringsWithSize(1))); client = createDefaultMockDdlClient(); batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); - batch.runBatch(); + batch.executeDdlAsync(statement); + batch.executeDdlAsync(statement); + get(batch.runBatchAsync()); verify(client).executeDdl(argThat(isListOfStringsWithSize(2))); assertThat(batch.getState(), is(UnitOfWorkState.RAN)); boolean exception = false; try { - batch.runBatch(); + get(batch.runBatchAsync()); } catch (SpannerException e) { if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { throw e; @@ -323,7 +328,7 @@ public void testRunBatch() { assertThat(batch.getState(), is(UnitOfWorkState.RAN)); exception = false; try { - batch.executeDdl(statement); + batch.executeDdlAsync(statement); } catch (SpannerException e) { if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { throw e; @@ -333,7 +338,7 @@ public void testRunBatch() { assertThat(exception, is(true)); exception = false; try { - batch.executeDdl(statement); + batch.executeDdlAsync(statement); } catch (SpannerException e) { if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { throw e; @@ -344,11 +349,11 @@ public void testRunBatch() { client = createDefaultMockDdlClient(true); batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); + batch.executeDdlAsync(statement); + batch.executeDdlAsync(statement); exception = false; try { - batch.runBatch(); + get(batch.runBatchAsync()); } catch (SpannerException e) { exception = true; } @@ -380,9 +385,9 @@ public void testUpdateCount() throws InterruptedException, ExecutionException { .setDdlClient(client) .setDatabaseClient(mock(DatabaseClient.class)) .build(); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE FOO"))); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE BAR"))); - long[] updateCounts = batch.runBatch(); + batch.executeDdlAsync(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE FOO"))); + batch.executeDdlAsync(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE BAR"))); + long[] updateCounts = get(batch.runBatchAsync()); assertThat(updateCounts.length, is(equalTo(2))); assertThat(updateCounts[0], is(equalTo(1L))); assertThat(updateCounts[1], is(equalTo(1L))); @@ -412,10 +417,48 @@ public void testFailedUpdateCount() throws InterruptedException, ExecutionExcept .setDdlClient(client) .setDatabaseClient(mock(DatabaseClient.class)) .build(); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE FOO"))); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE INVALID_TABLE"))); + batch.executeDdlAsync(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE FOO"))); + batch.executeDdlAsync( + StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE INVALID_TABLE"))); + try { + get(batch.runBatchAsync()); + fail("missing expected exception"); + } catch (SpannerBatchUpdateException e) { + assertThat(e.getUpdateCounts().length, is(equalTo(2))); + assertThat(e.getUpdateCounts()[0], is(equalTo(1L))); + assertThat(e.getUpdateCounts()[1], is(equalTo(0L))); + } + } + + @Test + public void testFailedAfterFirstStatement() throws InterruptedException, ExecutionException { + DdlClient client = mock(DdlClient.class); + UpdateDatabaseDdlMetadata metadata = + UpdateDatabaseDdlMetadata.newBuilder() + .addCommitTimestamps( + Timestamp.newBuilder().setSeconds(System.currentTimeMillis() * 1000L - 1L)) + .addAllStatements(Arrays.asList("CREATE TABLE FOO", "CREATE TABLE INVALID_TABLE")) + .build(); + ApiFuture metadataFuture = ApiFutures.immediateFuture(metadata); + @SuppressWarnings("unchecked") + OperationFuture operationFuture = mock(OperationFuture.class); + when(operationFuture.get()) + .thenThrow( + new ExecutionException( + "ddl statement failed", Status.INVALID_ARGUMENT.asRuntimeException())); + when(operationFuture.getMetadata()).thenReturn(metadataFuture); + when(client.executeDdl(argThat(isListOfStringsWithSize(2)))).thenReturn(operationFuture); + DdlBatch batch = + DdlBatch.newBuilder() + .withStatementExecutor(new StatementExecutor()) + .setDdlClient(client) + .setDatabaseClient(mock(DatabaseClient.class)) + .build(); + batch.executeDdlAsync(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE FOO"))); + batch.executeDdlAsync( + StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE INVALID_TABLE"))); try { - batch.runBatch(); + get(batch.runBatchAsync()); fail("missing expected exception"); } catch (SpannerBatchUpdateException e) { assertThat(e.getUpdateCounts().length, is(equalTo(2))); @@ -440,26 +483,26 @@ public void testAbort() { client = createDefaultMockDdlClient(); batch = createSubject(client); - batch.executeDdl(statement); + batch.executeDdlAsync(statement); batch.abortBatch(); verify(client, never()).executeDdl(anyListOf(String.class)); client = createDefaultMockDdlClient(); batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); + batch.executeDdlAsync(statement); + batch.executeDdlAsync(statement); batch.abortBatch(); verify(client, never()).executeDdl(anyListOf(String.class)); client = createDefaultMockDdlClient(); batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); + batch.executeDdlAsync(statement); + batch.executeDdlAsync(statement); batch.abortBatch(); verify(client, never()).executeDdl(anyListOf(String.class)); boolean exception = false; try { - batch.runBatch(); + get(batch.runBatchAsync()); } catch (SpannerException e) { if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { throw e; @@ -479,7 +522,7 @@ public void testCancel() { DdlClient client = createDefaultMockDdlClient(10000L); final DdlBatch batch = createSubject(client); - batch.executeDdl(statement); + batch.executeDdlAsync(statement); Executors.newSingleThreadScheduledExecutor() .schedule( new Runnable() { @@ -491,7 +534,7 @@ public void run() { 100, TimeUnit.MILLISECONDS); try { - batch.runBatch(); + get(batch.runBatchAsync()); fail("expected CANCELLED"); } catch (SpannerException e) { assertEquals(ErrorCode.CANCELLED, e.getErrorCode()); @@ -502,7 +545,7 @@ public void run() { public void testCommit() { DdlBatch batch = createSubject(); try { - batch.commit(); + batch.commitAsync(); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -513,7 +556,7 @@ public void testCommit() { public void testRollback() { DdlBatch batch = createSubject(); try { - batch.rollback(); + batch.rollbackAsync(); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java index e841601db7f..0f1ca38cd7f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java @@ -16,15 +16,16 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.Matchers.anyListOf; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.api.core.ApiFutures; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.SpannerException; @@ -47,8 +48,8 @@ public class DmlBatchTest { private DmlBatch createSubject() { UnitOfWork transaction = mock(UnitOfWork.class); - when(transaction.executeBatchUpdate(Arrays.asList(statement1, statement2))) - .thenReturn(new long[] {3L, 5L}); + when(transaction.executeBatchUpdateAsync(Arrays.asList(statement1, statement2))) + .thenReturn(ApiFutures.immediateFuture(new long[] {3L, 5L})); return createSubject(transaction); } @@ -63,7 +64,7 @@ private DmlBatch createSubject(UnitOfWork transaction) { public void testExecuteQuery() { DmlBatch batch = createSubject(); try { - batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); + batch.executeQueryAsync(mock(ParsedStatement.class), AnalyzeMode.NONE); fail("Expected exception"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -74,7 +75,7 @@ public void testExecuteQuery() { public void testExecuteDdl() { DmlBatch batch = createSubject(); try { - batch.executeDdl(mock(ParsedStatement.class)); + batch.executeDdlAsync(mock(ParsedStatement.class)); fail("Expected exception"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -84,7 +85,7 @@ public void testExecuteDdl() { @Test public void testGetReadTimestamp() { DmlBatch batch = createSubject(); - batch.runBatch(); + get(batch.runBatchAsync()); try { batch.getReadTimestamp(); fail("Expected exception"); @@ -102,7 +103,7 @@ public void testIsReadOnly() { @Test public void testGetCommitTimestamp() { DmlBatch batch = createSubject(); - batch.runBatch(); + get(batch.runBatchAsync()); try { batch.getCommitTimestamp(); fail("Expected exception"); @@ -111,22 +112,11 @@ public void testGetCommitTimestamp() { } } - @Test - public void testWrite() { - DmlBatch batch = createSubject(); - try { - batch.write(Mutation.newInsertBuilder("foo").build()); - fail("Expected exception"); - } catch (SpannerException e) { - assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); - } - } - @Test public void testWriteIterable() { DmlBatch batch = createSubject(); try { - batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); + batch.writeAsync(Arrays.asList(Mutation.newInsertBuilder("foo").build())); fail("Expected exception"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -138,7 +128,7 @@ public void testGetStateAndIsActive() { DmlBatch batch = createSubject(); assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); assertThat(batch.isActive(), is(true)); - batch.runBatch(); + get(batch.runBatchAsync()); assertThat(batch.getState(), is(UnitOfWorkState.RAN)); assertThat(batch.isActive(), is(false)); @@ -150,7 +140,8 @@ public void testGetStateAndIsActive() { assertThat(batch.isActive(), is(false)); UnitOfWork tx = mock(UnitOfWork.class); - doThrow(SpannerException.class).when(tx).executeBatchUpdate(anyListOf(ParsedStatement.class)); + when(tx.executeBatchUpdateAsync(anyListOf(ParsedStatement.class))) + .thenReturn(ApiFutures.immediateFailedFuture(mock(SpannerException.class))); batch = createSubject(tx); assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); assertThat(batch.isActive(), is(true)); @@ -158,10 +149,10 @@ public void testGetStateAndIsActive() { when(statement.getStatement()).thenReturn(Statement.of("UPDATE TEST SET COL1=2")); when(statement.getSqlWithoutComments()).thenReturn("UPDATE TEST SET COL1=2"); when(statement.getType()).thenReturn(StatementType.UPDATE); - batch.executeUpdate(statement); + get(batch.executeUpdateAsync(statement)); boolean exception = false; try { - batch.runBatch(); + get(batch.runBatchAsync()); } catch (SpannerException e) { exception = true; } @@ -174,7 +165,7 @@ public void testGetStateAndIsActive() { public void testCommit() { DmlBatch batch = createSubject(); try { - batch.commit(); + batch.commitAsync(); fail("Expected exception"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -185,7 +176,7 @@ public void testCommit() { public void testRollback() { DmlBatch batch = createSubject(); try { - batch.rollback(); + batch.rollbackAsync(); fail("Expected exception"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java index fae463ceb11..88cbcc108b9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java @@ -32,6 +32,7 @@ import com.google.cloud.spanner.connection.SqlScriptVerifier.SpannerGenericConnection; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import java.lang.reflect.Field; import java.nio.file.Files; @@ -40,6 +41,7 @@ import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.concurrent.TimeUnit; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -119,13 +121,27 @@ public void intercept( try { Field field = ReadWriteTransaction.class.getDeclaredField("txManager"); field.setAccessible(true); + Stopwatch watch = Stopwatch.createStarted(); + while (field.get(transaction) == null && watch.elapsed(TimeUnit.MILLISECONDS) < 100) { + Thread.sleep(1L); + } TransactionManager tx = (TransactionManager) field.get(transaction); + if (tx == null) { + return; + } Class cls = Class.forName("com.google.cloud.spanner.TransactionManagerImpl"); Class cls2 = Class.forName("com.google.cloud.spanner.SessionPool$AutoClosingTransactionManager"); Field delegateField = cls2.getDeclaredField("delegate"); delegateField.setAccessible(true); + watch = watch.reset().start(); + while (delegateField.get(tx) == null && watch.elapsed(TimeUnit.MILLISECONDS) < 100) { + Thread.sleep(1L); + } TransactionManager delegate = (TransactionManager) delegateField.get(tx); + if (delegate == null) { + return; + } Field stateField = cls.getDeclaredField("txnState"); stateField.setAccessible(true); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java index 118f596c868..ad8b2849a1b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; @@ -183,7 +184,7 @@ public void testExecuteDdl() { ParsedStatement ddl = mock(ParsedStatement.class); when(ddl.getType()).thenReturn(StatementType.DDL); try { - createSubject().executeDdl(ddl); + createSubject().executeDdlAsync(ddl); fail("Expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); @@ -195,18 +196,7 @@ public void testExecuteUpdate() { ParsedStatement update = mock(ParsedStatement.class); when(update.getType()).thenReturn(StatementType.UPDATE); try { - createSubject().executeUpdate(update); - fail("Expected exception"); - } catch (SpannerException ex) { - assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); - } - } - - @Test - public void testWrite() { - Mutation mutation = Mutation.newInsertBuilder("foo").build(); - try { - createSubject().write(mutation); + createSubject().executeUpdateAsync(update); fail("Expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); @@ -217,7 +207,7 @@ public void testWrite() { public void testWriteIterable() { Mutation mutation = Mutation.newInsertBuilder("foo").build(); try { - createSubject().write(Arrays.asList(mutation, mutation)); + createSubject().writeAsync(Arrays.asList(mutation, mutation)); fail("Expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); @@ -228,7 +218,7 @@ public void testWriteIterable() { public void testRunBatch() { ReadOnlyTransaction subject = createSubject(); try { - subject.runBatch(); + subject.runBatchAsync(); fail("Expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); @@ -249,7 +239,7 @@ public void testAbortBatch() { @Test public void testGetCommitTimestamp() { ReadOnlyTransaction transaction = createSubject(); - transaction.commit(); + get(transaction.commitAsync()); assertThat(transaction.getState(), is(UnitOfWorkState.COMMITTED)); try { transaction.getCommitTimestamp(); @@ -275,7 +265,7 @@ public void testExecuteQuery() { when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); ReadOnlyTransaction transaction = createSubject(staleness); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.NONE); + ResultSet rs = get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)); assertThat(rs, is(notNullValue())); assertThat(rs.getStats(), is(nullValue())); } @@ -308,11 +298,11 @@ public void testExecuteQueryWithOptionsTest() { .build(); ResultSet expectedWithOptions = DirectExecuteResultSet.ofResultSet(resWithOptions); assertThat( - transaction.executeQuery(parsedStatement, AnalyzeMode.NONE, option), + get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE, option)), is(equalTo(expectedWithOptions))); ResultSet expectedWithoutOptions = DirectExecuteResultSet.ofResultSet(resWithoutOptions); assertThat( - transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), + get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)), is(equalTo(expectedWithoutOptions))); } @@ -327,7 +317,7 @@ public void testPlanQuery() { when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); ReadOnlyTransaction transaction = createSubject(staleness); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PLAN); + ResultSet rs = get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.PLAN)); assertThat(rs, is(notNullValue())); // get all results and then get the stats while (rs.next()) { @@ -348,7 +338,7 @@ public void testProfileQuery() { when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); ReadOnlyTransaction transaction = createSubject(staleness); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PROFILE); + ResultSet rs = get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.PROFILE)); assertThat(rs, is(notNullValue())); // get all results and then get the stats while (rs.next()) { @@ -378,7 +368,9 @@ public void testGetReadTimestamp() { } } assertThat(expectedException, is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); + assertThat( + get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)), + is(notNullValue())); assertThat(transaction.getReadTimestamp(), is(notNullValue())); } } @@ -406,7 +398,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - transaction.commit(); + get(transaction.commitAsync()); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -417,13 +409,14 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); + assertThat( + get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)), is(notNullValue())); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - transaction.commit(); + get(transaction.commitAsync()); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -435,7 +428,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - transaction.rollback(); + get(transaction.rollbackAsync()); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); @@ -446,12 +439,13 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); + assertThat( + get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)), is(notNullValue())); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - transaction.rollback(); + get(transaction.rollbackAsync()); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java index 1a332ab438a..1e094eaeb6f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; @@ -172,7 +173,7 @@ public void testExecuteDdl() { ReadWriteTransaction transaction = createSubject(); try { - transaction.executeDdl(statement); + transaction.executeDdlAsync(statement); fail("Expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); @@ -183,7 +184,7 @@ public void testExecuteDdl() { public void testRunBatch() { ReadWriteTransaction subject = createSubject(); try { - subject.runBatch(); + subject.runBatchAsync(); fail("Expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); @@ -210,7 +211,7 @@ public void testExecuteQuery() { when(parsedStatement.getStatement()).thenReturn(statement); ReadWriteTransaction transaction = createSubject(); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.NONE); + ResultSet rs = get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)); assertThat(rs, is(notNullValue())); assertThat(rs.getStats(), is(nullValue())); } @@ -224,7 +225,7 @@ public void testPlanQuery() { when(parsedStatement.getStatement()).thenReturn(statement); ReadWriteTransaction transaction = createSubject(); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PLAN); + ResultSet rs = get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.PLAN)); assertThat(rs, is(notNullValue())); while (rs.next()) { // do nothing @@ -241,7 +242,7 @@ public void testProfileQuery() { when(parsedStatement.getStatement()).thenReturn(statement); ReadWriteTransaction transaction = createSubject(); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PROFILE); + ResultSet rs = get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.PROFILE)); assertThat(rs, is(notNullValue())); while (rs.next()) { // do nothing @@ -258,7 +259,7 @@ public void testExecuteUpdate() { when(parsedStatement.getStatement()).thenReturn(statement); ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeUpdate(parsedStatement), is(1L)); + assertThat(get(transaction.executeUpdateAsync(parsedStatement)), is(1L)); } @Test @@ -270,7 +271,7 @@ public void testGetCommitTimestampBeforeCommit() { when(parsedStatement.getStatement()).thenReturn(statement); ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeUpdate(parsedStatement), is(1L)); + assertThat(get(transaction.executeUpdateAsync(parsedStatement)), is(1L)); try { transaction.getCommitTimestamp(); fail("Expected exception"); @@ -288,8 +289,8 @@ public void testGetCommitTimestampAfterCommit() { when(parsedStatement.getStatement()).thenReturn(statement); ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeUpdate(parsedStatement), is(1L)); - transaction.commit(); + assertThat(get(transaction.executeUpdateAsync(parsedStatement)), is(1L)); + get(transaction.commitAsync()); assertThat(transaction.getCommitTimestamp(), is(notNullValue())); } @@ -303,7 +304,8 @@ public void testGetReadTimestamp() { when(parsedStatement.getStatement()).thenReturn(statement); ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); + assertThat( + get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)), is(notNullValue())); try { transaction.getReadTimestamp(); fail("Expected exception"); @@ -325,13 +327,14 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); + assertThat( + get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE)), is(notNullValue())); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - transaction.commit(); + get(transaction.commitAsync()); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -343,7 +346,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - transaction.rollback(); + get(transaction.rollbackAsync()); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); @@ -356,7 +359,7 @@ public void testState() { is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); try { - transaction.commit(); + get(transaction.commitAsync()); } catch (SpannerException e) { // ignore } @@ -372,7 +375,7 @@ public void testState() { is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); try { - transaction.commit(); + get(transaction.commitAsync()); } catch (AbortedException e) { // ignore } @@ -388,7 +391,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - transaction.commit(); + get(transaction.commitAsync()); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -452,11 +455,11 @@ public void testRetry() { .setDatabaseClient(client) .withStatementExecutor(new StatementExecutor()) .build(); - subject.executeUpdate(update1); - subject.executeUpdate(update2); + subject.executeUpdateAsync(update1); + subject.executeUpdateAsync(update2); boolean expectedException = false; try { - subject.commit(); + get(subject.commitAsync()); } catch (SpannerException e) { if (results == RetryResults.DIFFERENT && e.getErrorCode() == ErrorCode.ABORTED) { // expected diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java index e73eb8e0b2b..76ef62a21a1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.SpannerApiFutures.get; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.mockito.Matchers.anyListOf; @@ -45,12 +46,15 @@ import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionManager; import com.google.cloud.spanner.TransactionRunner; +import com.google.cloud.spanner.connection.StatementExecutor.StatementTimeout; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.cloud.spanner.connection.StatementParser.StatementType; +import com.google.common.base.Preconditions; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import com.google.spanner.v1.ResultSetStats; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -77,6 +81,20 @@ private enum CommitBehavior { ABORT; } + /** Creates a {@link StatementTimeout} that will never timeout. */ + static StatementTimeout nullTimeout() { + return new StatementTimeout(); + } + + /** Creates a {@link StatementTimeout} with the given duration. */ + static StatementTimeout timeout(long timeout, TimeUnit unit) { + Preconditions.checkArgument(timeout > 0L); + Preconditions.checkArgument(StatementTimeout.isValidTimeoutUnit(unit)); + StatementTimeout res = new StatementTimeout(); + res.setTimeoutValue(timeout, unit); + return res; + } + private static class SimpleTransactionManager implements TransactionManager { private TransactionState state; private Timestamp commitTimestamp; @@ -287,16 +305,6 @@ private SingleUseTransaction createSubject() { 0L); } - private SingleUseTransaction createSubjectWithTimeout(long timeout) { - return createSubject( - createDefaultMockDdlClient(), - false, - TimestampBound.strong(), - AutocommitDmlMode.TRANSACTIONAL, - CommitBehavior.SUCCEED, - timeout); - } - private SingleUseTransaction createSubject(AutocommitDmlMode dmlMode) { return createSubject( createDefaultMockDdlClient(), @@ -349,7 +357,7 @@ private SingleUseTransaction createSubject( new SimpleReadOnlyTransaction(staleness); when(dbClient.singleUseReadOnlyTransaction(staleness)).thenReturn(singleUse); - TransactionContext txContext = mock(TransactionContext.class); + final TransactionContext txContext = mock(TransactionContext.class); when(txContext.executeUpdate(Statement.of(VALID_UPDATE))).thenReturn(VALID_UPDATE_COUNT); when(txContext.executeUpdate(Statement.of(SLOW_UPDATE))) .thenAnswer( @@ -381,12 +389,17 @@ public TransactionRunner answer(InvocationOnMock invocation) { new TransactionRunner() { private Timestamp commitTimestamp; - @SuppressWarnings("unchecked") @Override public T run(TransactionCallable callable) { if (commitBehavior == CommitBehavior.SUCCEED) { + T res; + try { + res = callable.run(txContext); + } catch (Exception e) { + throw SpannerExceptionFactory.newSpannerException(e); + } this.commitTimestamp = Timestamp.now(); - return (T) Long.valueOf(1L); + return res; } else if (commitBehavior == CommitBehavior.FAIL) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.UNKNOWN, "commit failed"); @@ -420,9 +433,7 @@ public TransactionRunner allowNestedTransaction() { .setReadOnly(readOnly) .setReadOnlyStaleness(staleness) .setStatementTimeout( - timeout == 0L - ? StatementExecutor.StatementTimeout.nullTimeout() - : StatementExecutor.StatementTimeout.of(timeout, TimeUnit.MILLISECONDS)) + timeout == 0L ? nullTimeout() : timeout(timeout, TimeUnit.MILLISECONDS)) .withStatementExecutor(executor) .build(); } @@ -464,7 +475,7 @@ private List getTestTimestampBounds() { public void testCommit() { SingleUseTransaction subject = createSubject(); try { - subject.commit(); + subject.commitAsync(); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); @@ -475,7 +486,7 @@ public void testCommit() { public void testRollback() { SingleUseTransaction subject = createSubject(); try { - subject.rollback(); + subject.rollbackAsync(); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); @@ -486,7 +497,7 @@ public void testRollback() { public void testRunBatch() { SingleUseTransaction subject = createSubject(); try { - subject.runBatch(); + subject.runBatchAsync(); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); @@ -510,7 +521,7 @@ public void testExecuteDdl() { ParsedStatement ddl = createParsedDdl(sql); DdlClient ddlClient = createDefaultMockDdlClient(); SingleUseTransaction subject = createDdlSubject(ddlClient); - subject.executeDdl(ddl); + get(subject.executeDdlAsync(ddl)); verify(ddlClient).executeDdl(sql); } @@ -519,7 +530,7 @@ public void testExecuteQuery() { for (TimestampBound staleness : getTestTimestampBounds()) { for (AnalyzeMode analyzeMode : AnalyzeMode.values()) { SingleUseTransaction subject = createReadOnlySubject(staleness); - ResultSet rs = subject.executeQuery(createParsedQuery(VALID_QUERY), analyzeMode); + ResultSet rs = get(subject.executeQueryAsync(createParsedQuery(VALID_QUERY), analyzeMode)); assertThat(rs).isNotNull(); assertThat(subject.getReadTimestamp()).isNotNull(); assertThat(subject.getState()) @@ -537,7 +548,7 @@ public void testExecuteQuery() { for (TimestampBound staleness : getTestTimestampBounds()) { SingleUseTransaction subject = createReadOnlySubject(staleness); try { - subject.executeQuery(createParsedQuery(INVALID_QUERY), AnalyzeMode.NONE); + get(subject.executeQueryAsync(createParsedQuery(INVALID_QUERY), AnalyzeMode.NONE)); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); @@ -570,14 +581,15 @@ public void testExecuteQueryWithOptionsTest() { .withStatementExecutor(executor) .setReadOnlyStaleness(TimestampBound.strong()) .build(); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE, option)).isNotNull(); + assertThat(get(transaction.executeQueryAsync(parsedStatement, AnalyzeMode.NONE, option))) + .isNotNull(); } @Test public void testExecuteUpdate_Transactional_Valid() { ParsedStatement update = createParsedUpdate(VALID_UPDATE); SingleUseTransaction subject = createSubject(); - long updateCount = subject.executeUpdate(update); + long updateCount = get(subject.executeUpdateAsync(update)); assertThat(updateCount).isEqualTo(VALID_UPDATE_COUNT); assertThat(subject.getCommitTimestamp()).isNotNull(); assertThat(subject.getState()) @@ -589,7 +601,7 @@ public void testExecuteUpdate_Transactional_Invalid() { ParsedStatement update = createParsedUpdate(INVALID_UPDATE); SingleUseTransaction subject = createSubject(); try { - subject.executeUpdate(update); + get(subject.executeUpdateAsync(update)); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); @@ -602,7 +614,7 @@ public void testExecuteUpdate_Transactional_Valid_FailedCommit() { ParsedStatement update = createParsedUpdate(VALID_UPDATE); SingleUseTransaction subject = createSubject(CommitBehavior.FAIL); try { - subject.executeUpdate(update); + get(subject.executeUpdateAsync(update)); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); @@ -610,23 +622,11 @@ public void testExecuteUpdate_Transactional_Valid_FailedCommit() { } } - @Test - public void testExecuteUpdate_Transactional_Valid_AbortedCommit() { - ParsedStatement update = createParsedUpdate(VALID_UPDATE); - SingleUseTransaction subject = createSubject(CommitBehavior.ABORT); - // even though the transaction aborts at first, it will be retried and eventually succeed - long updateCount = subject.executeUpdate(update); - assertThat(updateCount).isEqualTo(VALID_UPDATE_COUNT); - assertThat(subject.getCommitTimestamp()).isNotNull(); - assertThat(subject.getState()) - .isEqualTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED); - } - @Test public void testExecuteUpdate_Partitioned_Valid() { ParsedStatement update = createParsedUpdate(VALID_UPDATE); SingleUseTransaction subject = createSubject(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - long updateCount = subject.executeUpdate(update); + long updateCount = get(subject.executeUpdateAsync(update)); assertThat(updateCount).isEqualTo(VALID_UPDATE_COUNT); assertThat(subject.getState()) .isEqualTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED); @@ -637,7 +637,7 @@ public void testExecuteUpdate_Partitioned_Invalid() { ParsedStatement update = createParsedUpdate(INVALID_UPDATE); SingleUseTransaction subject = createSubject(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); try { - subject.executeUpdate(update); + get(subject.executeUpdateAsync(update)); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); @@ -645,32 +645,11 @@ public void testExecuteUpdate_Partitioned_Invalid() { } } - @Test - public void testWrite() { - SingleUseTransaction subject = createSubject(); - subject.write(Mutation.newInsertBuilder("FOO").build()); - assertThat(subject.getCommitTimestamp()).isNotNull(); - assertThat(subject.getState()) - .isEqualTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED); - } - - @Test - public void testWriteFail() { - SingleUseTransaction subject = createSubject(CommitBehavior.FAIL); - try { - subject.write(Mutation.newInsertBuilder("FOO").build()); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); - assertThat(e.getMessage()).contains("commit failed"); - } - } - @Test public void testWriteIterable() { SingleUseTransaction subject = createSubject(); Mutation mutation = Mutation.newInsertBuilder("FOO").build(); - subject.write(Arrays.asList(mutation, mutation)); + get(subject.writeAsync(Arrays.asList(mutation, mutation))); assertThat(subject.getCommitTimestamp()).isNotNull(); assertThat(subject.getState()) .isEqualTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED); @@ -681,7 +660,7 @@ public void testWriteIterableFail() { SingleUseTransaction subject = createSubject(CommitBehavior.FAIL); Mutation mutation = Mutation.newInsertBuilder("FOO").build(); try { - subject.write(Arrays.asList(mutation, mutation)); + get(subject.writeAsync(Arrays.asList(mutation, mutation))); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); @@ -693,11 +672,12 @@ public void testWriteIterableFail() { public void testMultiUse() { for (TimestampBound staleness : getTestTimestampBounds()) { SingleUseTransaction subject = createReadOnlySubject(staleness); - ResultSet rs = subject.executeQuery(createParsedQuery(VALID_QUERY), AnalyzeMode.NONE); + ResultSet rs = + get(subject.executeQueryAsync(createParsedQuery(VALID_QUERY), AnalyzeMode.NONE)); assertThat(rs).isNotNull(); assertThat(subject.getReadTimestamp()).isNotNull(); try { - subject.executeQuery(createParsedQuery(VALID_QUERY), AnalyzeMode.NONE); + get(subject.executeQueryAsync(createParsedQuery(VALID_QUERY), AnalyzeMode.NONE)); fail("missing expected exception"); } catch (IllegalStateException e) { } @@ -707,81 +687,42 @@ public void testMultiUse() { ParsedStatement ddl = createParsedDdl(sql); DdlClient ddlClient = createDefaultMockDdlClient(); SingleUseTransaction subject = createDdlSubject(ddlClient); - subject.executeDdl(ddl); + get(subject.executeDdlAsync(ddl)); verify(ddlClient).executeDdl(sql); try { - subject.executeDdl(ddl); + get(subject.executeDdlAsync(ddl)); fail("missing expected exception"); } catch (IllegalStateException e) { } ParsedStatement update = createParsedUpdate(VALID_UPDATE); subject = createSubject(); - long updateCount = subject.executeUpdate(update); + long updateCount = get(subject.executeUpdateAsync(update)); assertThat(updateCount).isEqualTo(VALID_UPDATE_COUNT); assertThat(subject.getCommitTimestamp()).isNotNull(); try { - subject.executeUpdate(update); + get(subject.executeUpdateAsync(update)); fail("missing expected exception"); } catch (IllegalStateException e) { } subject = createSubject(); - subject.write(Mutation.newInsertBuilder("FOO").build()); + get(subject.writeAsync(Collections.singleton(Mutation.newInsertBuilder("FOO").build()))); assertThat(subject.getCommitTimestamp()).isNotNull(); try { - subject.write(Mutation.newInsertBuilder("FOO").build()); + get(subject.writeAsync(Collections.singleton(Mutation.newInsertBuilder("FOO").build()))); fail("missing expected exception"); } catch (IllegalStateException e) { } subject = createSubject(); Mutation mutation = Mutation.newInsertBuilder("FOO").build(); - subject.write(Arrays.asList(mutation, mutation)); + get(subject.writeAsync(Arrays.asList(mutation, mutation))); assertThat(subject.getCommitTimestamp()).isNotNull(); try { - subject.write(Arrays.asList(mutation, mutation)); + get(subject.writeAsync(Arrays.asList(mutation, mutation))); fail("missing expected exception"); } catch (IllegalStateException e) { } } - - @Test - public void testExecuteQueryWithTimeout() { - SingleUseTransaction subject = createSubjectWithTimeout(1L); - try { - subject.executeQuery(createParsedQuery(SLOW_QUERY), AnalyzeMode.NONE); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.DEADLINE_EXCEEDED) { - throw e; - } - } - assertThat(subject.getState()) - .isEqualTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMIT_FAILED); - try { - subject.getReadTimestamp(); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); - } - } - - @Test - public void testExecuteUpdateWithTimeout() { - SingleUseTransaction subject = createSubjectWithTimeout(1L); - try { - subject.executeUpdate(createParsedUpdate(SLOW_UPDATE)); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); - } - assertThat(subject.getState()) - .isEqualTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMIT_FAILED); - try { - subject.getCommitTimestamp(); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); - } - } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java index 3c0e9cf160e..afc0512b4ec 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java @@ -33,9 +33,11 @@ import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.connection.ConnectionImpl.LeakedConnectionException; import com.google.cloud.spanner.connection.SpannerPool.CheckAndCloseSpannersMode; -import com.google.cloud.spanner.connection.SpannerPool.SpannerPoolKey; +import com.google.common.base.Ticker; +import com.google.common.testing.FakeTicker; import java.io.ByteArrayOutputStream; import java.io.OutputStream; +import java.util.concurrent.TimeUnit; import java.util.logging.Handler; import java.util.logging.Logger; import java.util.logging.StreamHandler; @@ -62,12 +64,13 @@ public class SpannerPoolTest { private ConnectionOptions options6 = mock(ConnectionOptions.class); private SpannerPool createSubjectAndMocks() { - return createSubjectAndMocks(0L); + return createSubjectAndMocks(0L, Ticker.systemTicker()); } - private SpannerPool createSubjectAndMocks(long closeSpannerAfterMillisecondsUnused) { + private SpannerPool createSubjectAndMocks( + long closeSpannerAfterMillisecondsUnused, Ticker ticker) { SpannerPool pool = - new SpannerPool(closeSpannerAfterMillisecondsUnused) { + new SpannerPool(closeSpannerAfterMillisecondsUnused, ticker) { @Override Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { return mock(Spanner.class); @@ -340,73 +343,77 @@ public void testCloseUnusedSpanners() { verify(spanner3).close(); } - /** Allow the automatic close test to be run multiple times to ensure it is stable */ - private static final int NUMBER_OF_AUTOMATIC_CLOSE_TEST_RUNS = 1; - - private static final long TEST_AUTOMATIC_CLOSE_TIMEOUT = 2L; - private static final long SLEEP_BEFORE_VERIFICATION = 100L; + private static final long TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS = 60_000L; + private static final long TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS = + TimeUnit.NANOSECONDS.convert(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + private static final long MILLISECOND = TimeUnit.NANOSECONDS.convert(1L, TimeUnit.MILLISECONDS); @Test public void testAutomaticCloser() throws InterruptedException { - for (int testRun = 0; testRun < NUMBER_OF_AUTOMATIC_CLOSE_TEST_RUNS; testRun++) { - // create a pool that will close unused spanners after 5 milliseconds - SpannerPool pool = createSubjectAndMocks(TEST_AUTOMATIC_CLOSE_TIMEOUT); - Spanner spanner1; - Spanner spanner2; - Spanner spanner3; - - // create two connections that use the same Spanner - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options1, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - - // all spanners are in use, this should have no effect - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1, never()).close(); - - // close one connection. This should also have no effect. - pool.removeConnection(options1, connection1); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1, never()).close(); - - // close the other connection as well, the Spanner object should now be closed. - pool.removeConnection(options1, connection2); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - - // create three connections that use two different Spanners - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options2, connection2); - spanner3 = pool.getSpanner(options2, connection3); - assertThat(spanner1, not(equalTo(spanner2))); - assertThat(spanner2, is(equalTo(spanner3))); - - // all spanners are in use, this should have no effect - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1, never()).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection1. That should also mark spanner1 as no longer in use - pool.removeConnection(options1, connection1); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection2. That should have no effect, as connection3 is still using spanner2 - pool.removeConnection(options2, connection2); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection3. Now all should be closed. - pool.removeConnection(options2, connection3); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - verify(spanner2).close(); - verify(spanner3).close(); - } + FakeTicker ticker = new FakeTicker(); + SpannerPool pool = createSubjectAndMocks(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS, ticker); + Spanner spanner1; + Spanner spanner2; + Spanner spanner3; + + // create two connections that use the same Spanner + spanner1 = pool.getSpanner(options1, connection1); + spanner2 = pool.getSpanner(options1, connection2); + assertThat(spanner1, is(equalTo(spanner2))); + + // all spanners are in use, this should have no effect + ticker.advance(TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS + MILLISECOND); + pool.closeUnusedSpanners(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS); + verify(spanner1, never()).close(); + + // close one connection. This should also have no effect. + pool.removeConnection(options1, connection1); + ticker.advance(TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS + MILLISECOND); + pool.closeUnusedSpanners(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS); + verify(spanner1, never()).close(); + + // close the other connection as well, the Spanner object should now be closed. + pool.removeConnection(options1, connection2); + ticker.advance(TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS + MILLISECOND); + pool.closeUnusedSpanners(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS); + verify(spanner1).close(); + + // create three connections that use two different Spanners + spanner1 = pool.getSpanner(options1, connection1); + spanner2 = pool.getSpanner(options2, connection2); + spanner3 = pool.getSpanner(options2, connection3); + assertThat(spanner1, not(equalTo(spanner2))); + assertThat(spanner2, is(equalTo(spanner3))); + + // all spanners are in use, this should have no effect + ticker.advance(TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS + MILLISECOND); + pool.closeUnusedSpanners(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS); + verify(spanner1, never()).close(); + verify(spanner2, never()).close(); + verify(spanner3, never()).close(); + + // close connection1. That should also mark spanner1 as no longer in use + pool.removeConnection(options1, connection1); + ticker.advance(TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS + MILLISECOND); + pool.closeUnusedSpanners(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS); + verify(spanner1).close(); + verify(spanner2, never()).close(); + verify(spanner3, never()).close(); + + // close connection2. That should have no effect, as connection3 is still using spanner2 + pool.removeConnection(options2, connection2); + ticker.advance(TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS + MILLISECOND); + pool.closeUnusedSpanners(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS); + verify(spanner1).close(); + verify(spanner2, never()).close(); + verify(spanner3, never()).close(); + + // close connection3. Now all should be closed. + pool.removeConnection(options2, connection3); + ticker.advance(TEST_AUTOMATIC_CLOSE_TIMEOUT_NANOS + MILLISECOND); + pool.closeUnusedSpanners(TEST_AUTOMATIC_CLOSE_TIMEOUT_MILLIS); + verify(spanner1).close(); + verify(spanner2).close(); + verify(spanner3).close(); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java index e483a502792..eac4c38d177 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java @@ -22,59 +22,53 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.DatabaseClient; + +import com.google.api.gax.longrunning.OperationTimedPollAlgorithm; +import com.google.api.gax.retrying.RetrySettings; import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ReadOnlyTransaction; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions.Builder; import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.cloud.spanner.connection.AbstractConnectionImplTest.ConnectionConsumer; +import com.google.cloud.spanner.connection.ConnectionOptions.SpannerOptionsConfigurator; +import com.google.cloud.spanner.connection.ITAbstractSpannerTest.ITConnection; +import com.google.common.util.concurrent.Uninterruptibles; +import com.google.longrunning.Operation; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.Any; +import com.google.protobuf.Empty; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import java.util.Arrays; +import com.google.spanner.v1.CommitRequest; +import com.google.spanner.v1.ExecuteSqlRequest; +import io.grpc.Status; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Matchers; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import org.threeten.bp.Duration; @RunWith(JUnit4.class) -public class StatementTimeoutTest { +public class StatementTimeoutTest extends AbstractMockServerTest { - private static final String URI = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; private static final String SLOW_SELECT = "SELECT foo FROM bar"; private static final String INVALID_SELECT = "SELECT FROM bar"; // missing columns / * - private static final String FAST_SELECT = "SELECT fast_column FROM fast_table"; private static final String SLOW_DDL = "CREATE TABLE foo"; private static final String FAST_DDL = "CREATE TABLE fast_table"; private static final String SLOW_UPDATE = "UPDATE foo SET col1=1 WHERE id=2"; - private static final String FAST_UPDATE = "UPDATE fast_table SET foo=1 WHERE bar=2"; /** Execution time for statements that have been defined as slow. */ - private static final long EXECUTION_TIME_SLOW_STATEMENT = 10_000L; + private static final int EXECUTION_TIME_SLOW_STATEMENT = 10_000; /** * This timeout should be high enough that it will never be exceeded, even on a slow build * environment, but still significantly lower than the expected execution time of the slow @@ -87,193 +81,51 @@ public class StatementTimeoutTest { * still high enough that it would normally not be exceeded for a statement that is executed * directly. */ - private static final long TIMEOUT_FOR_SLOW_STATEMENTS = 20L; - /** - * The number of milliseconds to wait before cancelling a query should be high enough to not cause - * flakiness on a slow environment, but at the same time low enough that it does not slow down the - * test case unnecessarily. - */ - private static final int WAIT_BEFORE_CANCEL = 100; - - private enum CommitRollbackBehavior { - FAST, - SLOW_COMMIT, - SLOW_ROLLBACK; - } - - private static final class DelayedQueryExecution implements Answer { - @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return mock(ResultSet.class); - } - } - - private DdlClient createDefaultMockDdlClient(final long waitForMillis) { - try { - DdlClient ddlClient = mock(DdlClient.class); - UpdateDatabaseDdlMetadata metadata = UpdateDatabaseDdlMetadata.getDefaultInstance(); - ApiFuture futureMetadata = ApiFutures.immediateFuture(metadata); - @SuppressWarnings("unchecked") - final OperationFuture operation = - mock(OperationFuture.class); - if (waitForMillis > 0L) { - when(operation.get()) - .thenAnswer( - new Answer() { + private static final int TIMEOUT_FOR_SLOW_STATEMENTS = 20; + + ITConnection createConnection() { + StringBuilder url = new StringBuilder(getBaseUrl()); + ConnectionOptions options = + ConnectionOptions.newBuilder() + .setUri(url.toString()) + .setConfigurator( + new SpannerOptionsConfigurator() { @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(waitForMillis); - return null; + public void configure(Builder options) { + options + .getDatabaseAdminStubSettingsBuilder() + .updateDatabaseDdlOperationSettings() + .setPollingAlgorithm( + OperationTimedPollAlgorithm.create( + RetrySettings.newBuilder() + .setInitialRetryDelay(Duration.ofMillis(1L)) + .setMaxRetryDelay(Duration.ofMillis(1L)) + .setRetryDelayMultiplier(1.0) + .setTotalTimeout(Duration.ofMinutes(10L)) + .build())); } - }); - } else { - when(operation.get()).thenReturn(null); - } - when(operation.getMetadata()).thenReturn(futureMetadata); - when(ddlClient.executeDdl(SLOW_DDL)).thenCallRealMethod(); - when(ddlClient.executeDdl(anyListOf(String.class))).thenReturn(operation); - - @SuppressWarnings("unchecked") - final OperationFuture fastOperation = - mock(OperationFuture.class); - when(fastOperation.isDone()).thenReturn(true); - when(fastOperation.get()).thenReturn(null); - when(fastOperation.getMetadata()).thenReturn(futureMetadata); - when(ddlClient.executeDdl(FAST_DDL)).thenReturn(fastOperation); - when(ddlClient.executeDdl(Arrays.asList(FAST_DDL))).thenReturn(fastOperation); - return ddlClient; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private ConnectionImpl createConnection(ConnectionOptions options) { - return createConnection(options, CommitRollbackBehavior.FAST); + }) + .build(); + return createITConnection(options); } - /** - * Creates a connection on which the statements {@link StatementTimeoutTest#SLOW_SELECT} and - * {@link StatementTimeoutTest#SLOW_DDL} will take at least 10,000 milliseconds - */ - private ConnectionImpl createConnection( - ConnectionOptions options, final CommitRollbackBehavior commitRollbackBehavior) { - DatabaseClient dbClient = mock(DatabaseClient.class); - Spanner spanner = mock(Spanner.class); - SpannerPool spannerPool = mock(SpannerPool.class); - when(spannerPool.getSpanner(any(ConnectionOptions.class), any(ConnectionImpl.class))) - .thenReturn(spanner); - DdlClient ddlClient = createDefaultMockDdlClient(EXECUTION_TIME_SLOW_STATEMENT); - final ResultSet invalidResultSet = mock(ResultSet.class); - when(invalidResultSet.next()) - .thenThrow( - SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "invalid query")); - - ReadOnlyTransaction singleUseReadOnlyTx = mock(ReadOnlyTransaction.class); - when(singleUseReadOnlyTx.executeQuery(Statement.of(SLOW_SELECT))) - .thenAnswer(new DelayedQueryExecution()); - when(singleUseReadOnlyTx.executeQuery(Statement.of(FAST_SELECT))) - .thenReturn(mock(ResultSet.class)); - when(singleUseReadOnlyTx.executeQuery(Statement.of(INVALID_SELECT))) - .thenReturn(invalidResultSet); - when(dbClient.singleUseReadOnlyTransaction(Matchers.any(TimestampBound.class))) - .thenReturn(singleUseReadOnlyTx); - - ReadOnlyTransaction readOnlyTx = mock(ReadOnlyTransaction.class); - when(readOnlyTx.executeQuery(Statement.of(SLOW_SELECT))) - .thenAnswer(new DelayedQueryExecution()); - when(readOnlyTx.executeQuery(Statement.of(FAST_SELECT))).thenReturn(mock(ResultSet.class)); - when(readOnlyTx.executeQuery(Statement.of(INVALID_SELECT))).thenReturn(invalidResultSet); - when(dbClient.readOnlyTransaction(Matchers.any(TimestampBound.class))).thenReturn(readOnlyTx); - - when(dbClient.transactionManager()) - .thenAnswer( - new Answer() { - @Override - public TransactionManager answer(InvocationOnMock invocation) { - TransactionManager txManager = mock(TransactionManager.class); - when(txManager.getState()).thenReturn(null, TransactionState.STARTED); - when(txManager.begin()) - .thenAnswer( - new Answer() { - @Override - public TransactionContext answer(InvocationOnMock invocation) { - TransactionContext txContext = mock(TransactionContext.class); - when(txContext.executeQuery(Statement.of(SLOW_SELECT))) - .thenAnswer(new DelayedQueryExecution()); - when(txContext.executeQuery(Statement.of(FAST_SELECT))) - .thenReturn(mock(ResultSet.class)); - when(txContext.executeQuery(Statement.of(INVALID_SELECT))) - .thenReturn(invalidResultSet); - when(txContext.executeUpdate(Statement.of(SLOW_UPDATE))) - .thenAnswer( - new Answer() { - @Override - public Long answer(InvocationOnMock invocation) - throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return 1L; - } - }); - when(txContext.executeUpdate(Statement.of(FAST_UPDATE))).thenReturn(1L); - return txContext; - } - }); - if (commitRollbackBehavior == CommitRollbackBehavior.SLOW_COMMIT) { - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return null; - } - }) - .when(txManager) - .commit(); - } - if (commitRollbackBehavior == CommitRollbackBehavior.SLOW_ROLLBACK) { - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return null; - } - }) - .when(txManager) - .rollback(); - } - - return txManager; - } - }); - when(dbClient.executePartitionedUpdate(Statement.of(FAST_UPDATE))).thenReturn(1L); - when(dbClient.executePartitionedUpdate(Statement.of(SLOW_UPDATE))) - .thenAnswer( - new Answer() { - @Override - public Long answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return 1L; - } - }); - return new ConnectionImpl(options, spannerPool, ddlClient, dbClient); + @After + public void clearExecutionTimes() { + mockSpanner.removeAllExecutionTimes(); } @Test public void testTimeoutExceptionReadOnlyAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setReadOnly(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -282,44 +134,43 @@ public void testTimeoutExceptionReadOnlyAutocommit() { @Test public void testTimeoutExceptionReadOnlyAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setReadOnly(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // assert that multiple statements after each other also time out for (int i = 0; i < 2; i++) { - boolean timedOut = false; try { - connection.executeQuery(Statement.of(SLOW_SELECT)); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); } - assertThat(timedOut, is(true)); } // try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } } } @Test public void testTimeoutExceptionReadOnlyTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { connection.setReadOnly(true); connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -328,46 +179,45 @@ public void testTimeoutExceptionReadOnlyTransactional() { @Test public void testTimeoutExceptionReadOnlyTransactionMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { connection.setReadOnly(true); connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // assert that multiple statements after each other also time out for (int i = 0; i < 2; i++) { - boolean timedOut = false; try { - connection.executeQuery(Statement.of(SLOW_SELECT)); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); } - assertThat(timedOut, is(true)); } // do a rollback without any chance of a timeout connection.clearStatementTimeout(); connection.rollback(); // try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } } } @Test public void testTimeoutExceptionReadWriteAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -376,41 +226,41 @@ public void testTimeoutExceptionReadWriteAutocommit() { @Test public void testTimeoutExceptionReadWriteAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // assert that multiple statements after each other also time out for (int i = 0; i < 2; i++) { - boolean timedOut = false; try { - connection.executeQuery(Statement.of(SLOW_SELECT)); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); } - assertThat(timedOut, is(true)); } // try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } } } @Test public void testTimeoutExceptionReadWriteAutocommitSlowUpdate() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); try { - connection.execute(Statement.of(SLOW_UPDATE)); - fail("Expected exception"); + connection.execute(INSERT_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -419,44 +269,40 @@ public void testTimeoutExceptionReadWriteAutocommitSlowUpdate() { @Test public void testTimeoutExceptionReadWriteAutocommitSlowUpdateMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // assert that multiple statements after each other also time out for (int i = 0; i < 2; i++) { - boolean timedOut = false; try { connection.execute(Statement.of(SLOW_UPDATE)); + fail("missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); } - assertThat(timedOut, is(true)); } // try to do a new update that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.execute(Statement.of(FAST_UPDATE)).getUpdateCount(), is(equalTo(1L))); + assertThat(connection.execute(INSERT_STATEMENT).getUpdateCount(), is(equalTo(UPDATE_COUNT))); } } @Test public void testTimeoutExceptionReadWriteAutocommitSlowCommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); // First verify that the fast update does not timeout when in transactional mode (as it is the // commit that is slow). connection.setAutocommit(false); - connection.execute(Statement.of(FAST_UPDATE)); + connection.execute(INSERT_STATEMENT); connection.rollback(); // Then verify that the update does timeout when executed in autocommit mode, as the commit @@ -464,8 +310,8 @@ public void testTimeoutExceptionReadWriteAutocommitSlowCommit() { connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); connection.setAutocommit(true); try { - connection.execute(Statement.of(FAST_UPDATE)); - fail("Expected exception"); + connection.execute(INSERT_STATEMENT); + fail("missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -474,47 +320,47 @@ public void testTimeoutExceptionReadWriteAutocommitSlowCommit() { @Test public void testTimeoutExceptionReadWriteAutocommitSlowCommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // assert that multiple statements after each other also time out for (int i = 0; i < 2; i++) { - boolean timedOut = false; try { - connection.execute(Statement.of(FAST_UPDATE)); + connection.execute(INSERT_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertThat(e.getErrorCode(), is(equalTo(ErrorCode.DEADLINE_EXCEEDED))); } - assertThat(timedOut, is(true)); } - // try to do a new query that is fast. + // try to do a query in autocommit mode. This will use a single-use read-only transaction that + // does not need to commit, i.e. it should succeed. connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } } } @Test public void testTimeoutExceptionReadWriteAutocommitPartitioned() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setAutocommitDmlMode(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - // first verify that the fast update does not timeout + // First verify that the statement will not timeout by default. connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - connection.execute(Statement.of(FAST_UPDATE)); + connection.execute(INSERT_STATEMENT); + // Now slow down the execution and verify that it times out. PDML uses the ExecuteStreamingSql + // RPC. + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); try { - connection.execute(Statement.of(SLOW_UPDATE)); - fail("Expected exception"); + connection.execute(INSERT_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -523,17 +369,15 @@ public void testTimeoutExceptionReadWriteAutocommitPartitioned() { @Test public void testTimeoutExceptionReadWriteTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -542,57 +386,55 @@ public void testTimeoutExceptionReadWriteTransactional() { @Test public void testTimeoutExceptionReadWriteTransactionMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // Assert that multiple statements after each other will timeout the first time, and then // throw a SpannerException with code FAILED_PRECONDITION. - boolean timedOut = false; for (int i = 0; i < 2; i++) { try { - connection.executeQuery(Statement.of(SLOW_SELECT)); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException e) { if (i == 0) { assertThat(e.getErrorCode(), is(equalTo(ErrorCode.DEADLINE_EXCEEDED))); - timedOut = true; } else { assertThat(e.getErrorCode(), is(equalTo(ErrorCode.FAILED_PRECONDITION))); } } } - assertThat(timedOut, is(true)); // do a rollback without any chance of a timeout connection.clearStatementTimeout(); connection.rollback(); // try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } } } @Test public void testTimeoutExceptionReadWriteTransactionalSlowCommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - connection.executeQuery(Statement.of(FAST_SELECT)); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); try { connection.commit(); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -601,30 +443,27 @@ public void testTimeoutExceptionReadWriteTransactionalSlowCommit() { @Test public void testTimeoutExceptionReadWriteTransactionalSlowRollback() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_ROLLBACK)) { + mockSpanner.setRollbackExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - connection.executeQuery(Statement.of(FAST_SELECT)); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - try { - connection.rollback(); - fail("Expected exception"); - } catch (SpannerException ex) { - assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); } + connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); + // Rollback timeouts are not propagated as exceptions, as all errors during a Rollback RPC are + // ignored by the client library. + connection.rollback(); } } private static final class ConnectionReadOnlyAutocommit implements ConnectionConsumer { @Override public void accept(Connection t) { + t.setAutocommit(true); t.setReadOnly(true); } } @@ -651,7 +490,10 @@ public void testInterruptedExceptionReadOnlyTransactional() private static final class ConnectionReadWriteAutocommit implements ConnectionConsumer { @Override - public void accept(Connection t) {} + public void accept(Connection t) { + t.setAutocommit(true); + t.setReadOnly(false); + } } @Test @@ -664,6 +506,7 @@ private static final class ConnectionReadWriteTransactional implements Connectio @Override public void accept(Connection t) { t.setAutocommit(false); + t.setReadOnly(false); } } @@ -675,51 +518,45 @@ public void testInterruptedExceptionReadWriteTransactional() private void testInterruptedException(final ConnectionConsumer consumer) throws InterruptedException, ExecutionException { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + final CountDownLatch latch = new CountDownLatch(1); ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = executor.submit( new Callable() { @Override public Boolean call() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + try (Connection connection = createConnection()) { consumer.accept(connection); connection.setStatementTimeout(10000L, TimeUnit.MILLISECONDS); - connection.executeQuery(Statement.of(SLOW_SELECT)); + latch.countDown(); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) {} + return false; } catch (SpannerException e) { - if (e.getErrorCode() == ErrorCode.CANCELLED) { - return Boolean.TRUE; - } else { - return Boolean.FALSE; - } + return e.getErrorCode() == ErrorCode.CANCELLED; } - return Boolean.FALSE; } }); - // wait a little bit to ensure that the task has started - Thread.sleep(10L); + latch.await(10L, TimeUnit.SECONDS); executor.shutdownNow(); assertThat(future.get(), is(true)); } @Test public void testInvalidQueryReadOnlyAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setUri(URI) - .setCredentials(NoCredentials.getInstance()) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.INVALID_ARGUMENT.asRuntimeException())); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setReadOnly(true); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); try { connection.executeQuery(Statement.of(INVALID_SELECT)); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); } @@ -728,18 +565,16 @@ public void testInvalidQueryReadOnlyAutocommit() { @Test public void testInvalidQueryReadOnlyTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.INVALID_ARGUMENT.asRuntimeException())); + + try (Connection connection = createConnection()) { connection.setReadOnly(true); connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); try { connection.executeQuery(Statement.of(INVALID_SELECT)); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); } @@ -748,16 +583,15 @@ public void testInvalidQueryReadOnlyTransactional() { @Test public void testInvalidQueryReadWriteAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.INVALID_ARGUMENT.asRuntimeException())); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); try { connection.executeQuery(Statement.of(INVALID_SELECT)); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); } @@ -766,394 +600,427 @@ public void testInvalidQueryReadWriteAutocommit() { @Test public void testInvalidQueryReadWriteTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.INVALID_ARGUMENT.asRuntimeException())); + + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); try { connection.executeQuery(Statement.of(INVALID_SELECT)); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); } } } + static void waitForRequestsToContain(Class request) { + try { + mockSpanner.waitForRequestsToContain(request, EXECUTION_TIME_SLOW_STATEMENT); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + throw SpannerExceptionFactory.propagateTimeout(e); + } + } + @Test public void testCancelReadOnlyAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setReadOnly(true); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } + } finally { + executor.shutdown(); } } @Test public void testCancelReadOnlyAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); - boolean cancelled = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(true); + connection.setReadOnly(true); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); + + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + fail("Missing expected exception"); } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; + assertThat(e.getErrorCode(), is(equalTo(ErrorCode.CANCELLED))); } - assertThat(cancelled, is(true)); - // try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } + } finally { + executor.shutdown(); } } @Test public void testCancelReadOnlyTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { connection.setReadOnly(true); connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } + } finally { + executor.shutdown(); } } @Test public void testCancelReadOnlyTransactionalMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { connection.setReadOnly(true); connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - boolean cancelled = false; + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); try { connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Missing expected exception"); } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; + assertEquals(ErrorCode.CANCELLED, e.getErrorCode()); } - assertThat(cancelled, is(true)); // try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } // rollback and do another fast query connection.rollback(); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } + } finally { + executor.shutdown(); } } @Test public void testCancelReadWriteAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(true); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } + } finally { + executor.shutdown(); } } @Test public void testCancelReadWriteAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); - boolean cancelled = false; + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(true); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } - assertThat(cancelled, is(true)); // try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } + } finally { + executor.shutdown(); } } @Test public void testCancelReadWriteAutocommitSlowUpdate() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + mockSpanner.setExecuteSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(true); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); try { - connection.execute(Statement.of(SLOW_UPDATE)); - fail("Expected exception"); + connection.execute(INSERT_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } + } finally { + executor.shutdown(); } } @Test public void testCancelReadWriteAutocommitSlowCommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - connection.execute(Statement.of(FAST_UPDATE)); - fail("Expected exception"); + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(true); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(CommitRequest.class); + connection.cancel(); + } + }); + connection.execute(INSERT_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } finally { + executor.shutdown(); } } @Test public void testCancelReadWriteTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(false); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } finally { + executor.shutdown(); } } @Test public void testCancelReadWriteTransactionalMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); - boolean cancelled = false; + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(false); + executor.execute( + new Runnable() { + @Override + public void run() { + waitForRequestsToContain(ExecuteSqlRequest.class); + connection.cancel(); + } + }); try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - fail("Expected exception"); + connection.executeQuery(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; + assertEquals(ErrorCode.CANCELLED, e.getErrorCode()); } - assertThat(cancelled, is(true)); // Rollback the transaction as it is no longer usable. connection.rollback(); // Try to do a new query that is fast. + mockSpanner.removeAllExecutionTimes(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); + try (ResultSet rs = connection.executeQuery(SELECT_RANDOM_STATEMENT)) { + assertThat(rs, is(notNullValue())); + } + } finally { + executor.shutdown(); + } + } + + static void addSlowMockDdlOperation() { + addSlowMockDdlOperations(1); + } + + static void addSlowMockDdlOperations(int count) { + addMockDdlOperations(count, false); + } + + static void addFastMockDdlOperation() { + addFastMockDdlOperations(1); + } + + static void addFastMockDdlOperations(int count) { + addMockDdlOperations(count, true); + } + + static void addMockDdlOperations(int count, boolean done) { + for (int i = 0; i < count; i++) { + mockDatabaseAdmin.addResponse( + Operation.newBuilder() + .setMetadata( + Any.pack( + UpdateDatabaseDdlMetadata.newBuilder() + .addStatements(SLOW_DDL) + .setDatabase("projects/proj/instances/inst/databases/db") + .build())) + .setName("projects/proj/instances/inst/databases/db/operations/1") + .setDone(done) + .setResponse(Any.pack(Empty.getDefaultInstance())) + .build()); } } @Test public void testCancelDdlBatch() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + addSlowMockDdlOperation(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.startBatchDdl(); connection.execute(Statement.of(SLOW_DDL)); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + executor.execute( + new Runnable() { + @Override + public void run() { + Uninterruptibles.sleepUninterruptibly(100L, TimeUnit.MILLISECONDS); + connection.cancel(); + } + }); connection.runBatch(); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } finally { + executor.shutdown(); } } @Test public void testCancelDdlAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); + addSlowMockDdlOperation(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try (Connection connection = createConnection()) { + connection.setAutocommit(true); + executor.execute( + new Runnable() { + @Override + public void run() { + Uninterruptibles.sleepUninterruptibly(100L, TimeUnit.MILLISECONDS); + connection.cancel(); + } + }); connection.execute(Statement.of(SLOW_DDL)); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } finally { + executor.shutdown(); } } @Test public void testTimeoutExceptionDdlAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + addSlowMockDdlOperations(10); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); connection.execute(Statement.of(SLOW_DDL)); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -1161,25 +1028,24 @@ public void testTimeoutExceptionDdlAutocommit() { @Test public void testTimeoutExceptionDdlAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + addSlowMockDdlOperations(20); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // assert that multiple statements after each other also time out for (int i = 0; i < 2; i++) { - boolean timedOut = false; try { connection.execute(Statement.of(SLOW_DDL)); + fail("Missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); } - assertThat(timedOut, is(true)); } // try to do a new DDL statement that is fast. + mockDatabaseAdmin.reset(); + addFastMockDdlOperation(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); assertThat(connection.execute(Statement.of(FAST_DDL)), is(notNullValue())); } @@ -1187,21 +1053,18 @@ public void testTimeoutExceptionDdlAutocommitMultipleStatements() { @Test public void testTimeoutExceptionDdlBatch() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + addSlowMockDdlOperations(10); + + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.startBatchDdl(); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // the following statement will NOT timeout as the statement is only buffered locally connection.execute(Statement.of(SLOW_DDL)); - // the commit sends the statement to the server and should timeout + // the runBatch() statement sends the statement to the server and should timeout connection.runBatch(); - fail("Expected exception"); + fail("Missing expected exception"); } catch (SpannerException ex) { assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } @@ -1209,28 +1072,27 @@ public void testTimeoutExceptionDdlBatch() { @Test public void testTimeoutExceptionDdlBatchMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + addSlowMockDdlOperations(20); + + try (Connection connection = createConnection()) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); // assert that multiple statements after each other also time out for (int i = 0; i < 2; i++) { - boolean timedOut = false; + connection.startBatchDdl(); connection.execute(Statement.of(SLOW_DDL)); try { connection.runBatch(); + fail("Missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); } - assertThat(timedOut, is(true)); } // try to do a new DDL statement that is fast. + mockDatabaseAdmin.reset(); + addFastMockDdlOperation(); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); connection.startBatchDdl(); assertThat(connection.execute(Statement.of(FAST_DDL)), is(notNullValue())); @@ -1240,21 +1102,19 @@ public void testTimeoutExceptionDdlBatchMultipleStatements() { @Test public void testTimeoutDifferentTimeUnits() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(EXECUTION_TIME_SLOW_STATEMENT, 0)); + + try (Connection connection = createConnection()) { + connection.setAutocommit(true); for (TimeUnit unit : ReadOnlyStalenessUtil.SUPPORTED_UNITS) { connection.setStatementTimeout(1L, unit); - boolean timedOut = false; try { - connection.execute(Statement.of(SLOW_SELECT)); + connection.execute(SELECT_RANDOM_STATEMENT); + fail("Missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); } - assertThat(timedOut, is(true)); } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITAsyncTransactionRetryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITAsyncTransactionRetryTest.java new file mode 100644 index 00000000000..721dccc6512 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITAsyncTransactionRetryTest.java @@ -0,0 +1,1015 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection.it; + +import static com.google.cloud.spanner.SpannerApiFutures.get; +import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; + +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; +import com.google.cloud.spanner.AbortedException; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Options; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.connection.Connection; +import com.google.cloud.spanner.connection.ITAbstractSpannerTest; +import com.google.cloud.spanner.connection.TransactionRetryListener; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * This integration test tests the different scenarios for automatically retrying read/write + * transactions, both when possible and when the transaction must abort because of a concurrent + * update. + */ +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITAsyncTransactionRetryTest extends ITAbstractSpannerTest { + private static final Logger logger = + Logger.getLogger(ITAsyncTransactionRetryTest.class.getName()); + + @Rule public TestName testName = new TestName(); + + private static final ExecutorService executor = Executors.newFixedThreadPool(4); + + @AfterClass + public static void shutdownExecutor() { + executor.shutdown(); + } + + @Override + protected void appendConnectionUri(StringBuilder uri) { + uri.append(";autocommit=false;retryAbortsInternally=true"); + } + + @Override + public boolean doCreateDefaultTestTable() { + return true; + } + + /** Clear the test table before each test run */ + @Before + public void clearTable() { + try (ITConnection connection = createConnection()) { + connection.bufferedWrite(Mutation.delete("TEST", KeySet.all())); + get(connection.commitAsync()); + } + } + + @Before + public void clearStatistics() { + RETRY_STATISTICS.clear(); + } + + @Before + public void logStart() { + logger.fine( + "--------------------------------------------------------------\n" + + testName.getMethodName() + + " started"); + } + + @After + public void logFinished() { + logger.fine( + "--------------------------------------------------------------\n" + + testName.getMethodName() + + " finished"); + } + + /** Simple data structure to keep track of retry statistics */ + private static class RetryStatistics { + private int totalRetryAttemptsStarted; + private int totalRetryAttemptsFinished; + private int totalSuccessfulRetries; + private int totalErroredRetries; + private int totalNestedAborts; + private int totalMaxAttemptsExceeded; + private int totalConcurrentModifications; + + private void clear() { + totalRetryAttemptsStarted = 0; + totalRetryAttemptsFinished = 0; + totalSuccessfulRetries = 0; + totalErroredRetries = 0; + totalNestedAborts = 0; + totalMaxAttemptsExceeded = 0; + totalConcurrentModifications = 0; + } + } + + /** + * Static to allow access from the {@link CountTransactionRetryListener}. Statistics are + * automatically cleared before each test case. + */ + public static final RetryStatistics RETRY_STATISTICS = new RetryStatistics(); + + /** + * Simple {@link TransactionRetryListener} that keeps track of the total count of the different + * transaction retry events of a {@link Connection}. Note that as {@link + * TransactionRetryListener}s are instantiated once per connection, the listener keeps track of + * the total statistics of a connection and not only of the last transaction. + */ + public static class CountTransactionRetryListener implements TransactionRetryListener { + + @Override + public void retryStarting(Timestamp transactionStarted, long transactionId, int retryAttempt) { + RETRY_STATISTICS.totalRetryAttemptsStarted++; + } + + @Override + public void retryFinished( + Timestamp transactionStarted, long transactionId, int retryAttempt, RetryResult result) { + RETRY_STATISTICS.totalRetryAttemptsFinished++; + switch (result) { + case RETRY_ABORTED_AND_MAX_ATTEMPTS_EXCEEDED: + RETRY_STATISTICS.totalMaxAttemptsExceeded++; + break; + case RETRY_ABORTED_AND_RESTARTING: + RETRY_STATISTICS.totalNestedAborts++; + break; + case RETRY_ABORTED_DUE_TO_CONCURRENT_MODIFICATION: + RETRY_STATISTICS.totalConcurrentModifications++; + break; + case RETRY_ERROR: + RETRY_STATISTICS.totalErroredRetries++; + break; + case RETRY_SUCCESSFUL: + RETRY_STATISTICS.totalSuccessfulRetries++; + break; + default: + break; + } + } + } + + private ApiFuture getTestRecordCountAsync(Connection connection) { + final SettableApiFuture count = SettableApiFuture.create(); + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + count.set(resultSet.getLong("C")); + break; + } + } + } + }); + } + return count; + } + + private void verifyRecordCount(Connection connection, long expected) { + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT COUNT(*) AS C FROM TEST"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("C")).isEqualTo(expected); + assertThat(rs.next()).isFalse(); + } + } + + /** Test successful retry when the commit aborts */ + @Test + public void testCommitAborted() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + ApiFuture count = getTestRecordCountAsync(connection); + // do an insert + ApiFuture updateCount = + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); + // indicate that the next statement should abort + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + // do a commit that will first abort, and then on retry will succeed + ApiFuture commit = connection.commitAsync(); + + assertThat(get(count)).isEqualTo(0L); + // Wait until the commit has finished before checking retry stats. + assertThat(get(commit)).isNull(); + assertThat(get(updateCount)).isEqualTo(1L); + assertThat(RETRY_STATISTICS.totalRetryAttemptsStarted >= 1).isTrue(); + assertThat(RETRY_STATISTICS.totalRetryAttemptsFinished >= 1).isTrue(); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1).isTrue(); + assertThat(RETRY_STATISTICS.totalErroredRetries).isEqualTo(0); + assertThat(RETRY_STATISTICS.totalConcurrentModifications).isEqualTo(0); + assertThat(RETRY_STATISTICS.totalMaxAttemptsExceeded).isEqualTo(0); + // verify that the insert succeeded + verifyRecordCount(connection, 1L); + } + } + + /** Test successful retry when an insert statement aborts */ + @Test + public void testInsertAborted() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + ApiFuture count = getTestRecordCountAsync(connection); + // indicate that the next statement should abort + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + // do an insert that will abort + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); + // do a commit + ApiFuture commit = connection.commitAsync(); + assertThat(get(count)).isEqualTo(0L); + assertThat(get(commit)).isNull(); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1).isTrue(); + // verify that the insert succeeded + verifyRecordCount(connection, 1L); + } + } + + /** Test successful retry when an update statement aborts */ + @Test + public void testUpdateAborted() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + ApiFuture count = getTestRecordCountAsync(connection); + // insert a test record + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); + // indicate that the next statement should abort + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + // do an update that will abort + connection.executeUpdateAsync( + Statement.of("UPDATE TEST SET NAME='update aborted' WHERE ID=1")); + // do a commit + ApiFuture commit = connection.commitAsync(); + assertThat(get(count)).isEqualTo(0L); + assertThat(get(commit)).isNull(); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1).isTrue(); + // verify that the update succeeded + try (AsyncResultSet rs = + connection.executeQueryAsync( + Statement.of( + "SELECT COUNT(*) AS C FROM TEST WHERE ID=1 AND NAME='update aborted'"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("C")).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + } + + /** Test successful retry when a query aborts */ + @Test + public void testQueryAborted() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // insert a test record + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); + // indicate that the next statement should abort + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + // do a query that will abort + final SettableApiFuture countAfterInsert = SettableApiFuture.create(); + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + countAfterInsert.set(resultSet.getLong("C")); + break; + } + } + } catch (Throwable t) { + countAfterInsert.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + connection.commitAsync(); + assertThat(get(countAfterInsert)).isEqualTo(1L); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1).isTrue(); + // verify that the update succeeded + try (ResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("C")).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + } + + /** Test successful retry when a call to {@link ResultSet#next()} aborts */ + @Test + public void testNextCallAborted() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // insert two test records + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + // do a query + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + // the first record should be accessible without any problems + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("ID")).isEqualTo(1L); + + // indicate that the next statement should abort + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("ID")).isEqualTo(2L); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1).isTrue(); + // there should be only two records + assertThat(rs.next()).isFalse(); + } + connection.commitAsync(); + // verify that the transaction succeeded + verifyRecordCount(connection, 2L); + } + } + + /** Test successful retry after multiple aborts */ + @Test + public void testMultipleAborts() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + ApiFuture count = getTestRecordCountAsync(connection); + // do three inserts which all will abort and retry + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + get( + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')"))); + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + get( + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')"))); + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + get( + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')"))); + + ApiFuture commit = connection.commitAsync(); + assertThat(get(count)).isEqualTo(0L); + assertThat(get(commit)).isNull(); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries).isAtLeast(3); + // verify that the inserts succeeded + verifyRecordCount(connection, 3L); + } + } + + /** + * Tests that a transaction retry can be successful after a select, as long as the select returns + * the same results during the retry + */ + @Test + public void testAbortAfterSelect() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + ApiFuture count = getTestRecordCountAsync(connection); + // insert a test record + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + // select the test record + final SettableApiFuture initialRecord = SettableApiFuture.create(); + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST WHERE ID=1"))) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + initialRecord.set(resultSet.getCurrentRowAsStruct()); + } + } + } catch (Throwable t) { + initialRecord.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + // do another insert that will abort and retry + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + + // select the first test record again + final SettableApiFuture secondRecord = SettableApiFuture.create(); + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST WHERE ID=1"))) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + secondRecord.set(resultSet.getCurrentRowAsStruct()); + } + } + } catch (Throwable t) { + secondRecord.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + ApiFuture commit = connection.commitAsync(); + assertThat(get(count)).isEqualTo(0L); + assertThat(get(initialRecord)).isEqualTo(get(secondRecord)); + assertThat(get(commit)).isNull(); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1).isTrue(); + } + } + + /** + * Test a successful retry when a {@link ResultSet} has been consumed half way. The {@link + * ResultSet} should still be at the same position and still behave as if the original transaction + * did not abort. + */ + @Test + public void testAbortWithResultSetHalfway() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // insert two test records + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + // select the test records + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + // iterate one step + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("ID")).isEqualTo(1L); + // do another insert that will abort and retry + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); + // iterate another step + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("ID")).isEqualTo(2L); + // ensure we are at the end of the result set + assertThat(rs.next()).isFalse(); + } + get(connection.commitAsync()); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries).isAtLeast(1); + // verify that all the inserts succeeded + verifyRecordCount(connection, 3L); + } + } + + /** Test successful retry after a {@link ResultSet} has been fully consumed. */ + @Test + public void testAbortWithResultSetFullyConsumed() { + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // insert two test records + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + // select the test records and iterate over them + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + // do nothing, just consume the result set + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + break; + } + } + } + }); + } + // do another insert that will abort and retry + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); + get(connection.commitAsync()); + + assertThat(RETRY_STATISTICS.totalSuccessfulRetries).isAtLeast(1); + // verify that all the inserts succeeded + verifyRecordCount(connection, 3L); + } + } + + @Test + public void testAbortWithConcurrentInsert() { + assumeFalse("concurrent transactions are not supported on the emulator", isUsingEmulator()); + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // insert two test records + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + // select the test records and consume the entire result set + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + get( + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + break; + } + } + } + })); + } + // open a new connection and transaction and do an additional insert + try (ITConnection connection2 = createConnection()) { + connection2.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); + get(connection2.commitAsync()); + } + // now try to do an insert that will abort. The retry should now fail as there has been a + // concurrent modification + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + ApiFuture updateCount = + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (4, 'test 4')")); + try { + get(updateCount); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertRetryStatistics(1, 1, 0); + } + } + } + + @Test + public void testAbortWithConcurrentDelete() { + assumeFalse("concurrent transactions are not supported on the emulator", isUsingEmulator()); + AbortInterceptor interceptor = new AbortInterceptor(0); + // first insert two test records + try (ITConnection connection = createConnection()) { + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + get(connection.commitAsync()); + } + // open a new connection and select the two test records + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // select the test records and consume the entire result set + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + get( + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + break; + } + } + } + })); + } + // open a new connection and transaction and remove one of the test records + try (ITConnection connection2 = createConnection()) { + connection2.executeUpdateAsync(Statement.of("DELETE FROM TEST WHERE ID=1")); + get(connection2.commitAsync()); + } + // now try to do an insert that will abort. The retry should now fail as there has been a + // concurrent modification + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + try { + get( + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')"))); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertRetryStatistics(1, 1, 0); + } + } + } + + @Test + public void testAbortWithConcurrentUpdate() { + assumeFalse("concurrent transactions are not supported on the emulator", isUsingEmulator()); + AbortInterceptor interceptor = new AbortInterceptor(0); + // first insert two test records + try (ITConnection connection = createConnection()) { + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + get(connection.commitAsync()); + } + // open a new connection and select the two test records + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // select the test records and consume the entire result set + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + get( + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + break; + } + } + } + })); + } + // open a new connection and transaction and update one of the test records + try (ITConnection connection2 = createConnection()) { + connection2.executeUpdateAsync( + Statement.of("UPDATE TEST SET NAME='test updated' WHERE ID=2")); + get(connection2.commitAsync()); + } + // now try to do an insert that will abort. The retry should now fail as there has been a + // concurrent modification + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + try { + get( + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')"))); + fail("Missing expected exception"); + } catch (AbortedDueToConcurrentModificationException e) { + assertRetryStatistics(1, 1, 0); + } + } + } + + /** + * Test that shows that a transaction retry is possible even when there is a concurrent insert + * that has an impact on a query that has been executed, as long as the user hasn't actually seen + * the relevant part of the result of the query + */ + @Test + public void testAbortWithUnseenConcurrentInsert() throws InterruptedException { + assumeFalse("concurrent transactions are not supported on the emulator", isUsingEmulator()); + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // insert three test records + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); + // select the test records and consume part of the result set + final AtomicInteger count = new AtomicInteger(); + final AtomicLong lastSeenId = new AtomicLong(); + final CountDownLatch latch1 = new CountDownLatch(1); + final CountDownLatch latch2 = new CountDownLatch(1); + // Use buffer size 1. This means that the underlying result set will see 2 records (1 in the + // buffer and 1 waiting to be put in the buffer). + try (AsyncResultSet rs = + connection.executeQueryAsync( + Statement.of("SELECT * FROM TEST ORDER BY ID"), Options.bufferRows(1))) { + ApiFuture finished = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + count.incrementAndGet(); + lastSeenId.set(resultSet.getLong("ID")); + break; + } + if (count.get() == 1) { + // Let the other transaction proceed. + latch1.countDown(); + // Wait until the transaction has been aborted and retried. + if (!latch2.await(120L, TimeUnit.SECONDS)) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.DEADLINE_EXCEEDED, "Timeout while waiting for latch2"); + } + } + } + } catch (Throwable t) { + throw SpannerExceptionFactory.asSpannerException(t); + } + } + }); + // Open a new connection and transaction and do an additional insert. This insert will be + // included in a retry of the above query, but this has not yet been 'seen' by the user, + // hence is not a problem for retrying the transaction. + try (ITConnection connection2 = createConnection()) { + assertThat(latch1.await(60L, TimeUnit.SECONDS)).isTrue(); + connection2.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (4, 'test 4')")); + get(connection2.commitAsync()); + } + // now try to do an insert that will abort. The retry should still succeed. + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + int currentRetryCount = RETRY_STATISTICS.totalRetryAttemptsStarted; + get( + connection.executeUpdateAsync( + Statement.of("INSERT INTO TEST (ID, NAME) VALUES (5, 'test 5')"))); + assertThat(RETRY_STATISTICS.totalRetryAttemptsStarted).isAtLeast(currentRetryCount + 1); + // Consume the rest of the result set. The insert by the other transaction should now be + // included in the result set as the transaction retried. Although this means that the + // result + // is different after a retry, it is not different as seen by the user, as the user didn't + // know that the result set did not have any more results before the transaction retry. + latch2.countDown(); + get(finished); + // record with id 5 should not be visible, as it was added to the transaction after the + // query + // was executed + assertThat(count.get()).isEqualTo(4); + assertThat(lastSeenId.get()).isEqualTo(4L); + } + get(connection.commitAsync()); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries).isAtLeast(1); + } + } + + /** Test the successful retry of a transaction with a large {@link ResultSet} */ + @Test + public void testRetryLargeResultSet() { + final int NUMBER_OF_TEST_RECORDS = 100000; + final long UPDATED_RECORDS = 1000L; + AbortInterceptor interceptor = new AbortInterceptor(0); + try (ITConnection connection = createConnection()) { + // insert test records + for (int i = 0; i < NUMBER_OF_TEST_RECORDS; i++) { + connection.bufferedWrite( + Mutation.newInsertBuilder("TEST").set("ID").to(i).set("NAME").to("test " + i).build()); + if (i % 1000 == 0) { + connection.commitAsync(); + } + } + get(connection.commitAsync()); + } + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // select the test records and iterate over them + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + ApiFuture finished = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + // do nothing, just consume the result set + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + break; + } + } + } + }); + // Wait until the entire result set has been consumed. + get(finished); + } + // Do an update that will abort and retry. + interceptor.setProbability(1.0); + interceptor.setOnlyInjectOnce(true); + connection.executeUpdateAsync( + Statement.newBuilder("UPDATE TEST SET NAME='updated' WHERE ID<@max_id") + .bind("max_id") + .to(UPDATED_RECORDS) + .build()); + connection.commitAsync(); + // verify that the update succeeded + try (AsyncResultSet rs = + connection.executeQueryAsync( + Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE NAME='updated'"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("C")).isEqualTo(UPDATED_RECORDS); + assertThat(rs.next()).isFalse(); + } + // Verify that the transaction retried. + assertRetryStatistics(1, 0, 1); + } + } + + /** Test the successful retry of a transaction with a high chance of multiple aborts */ + @Test + public void testRetryHighAbortRate() { + final int NUMBER_OF_TEST_RECORDS = 10000; + final long UPDATED_RECORDS = 1000L; + // abort on 25% of all statements + AbortInterceptor interceptor = new AbortInterceptor(0.25D); + try (ITConnection connection = + createConnection(interceptor, new CountTransactionRetryListener())) { + // insert test records + for (int i = 0; i < NUMBER_OF_TEST_RECORDS; i++) { + connection.bufferedWrite( + Mutation.newInsertBuilder("TEST").set("ID").to(i).set("NAME").to("test " + i).build()); + if (i % 1000 == 0) { + connection.commitAsync(); + } + } + connection.commitAsync(); + // select the test records and iterate over them + // reduce the abort rate to 0.01% as each next() call could abort + interceptor.setProbability(0.0001D); + try (AsyncResultSet rs = + connection.executeQueryAsync(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { + ApiFuture finished = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + // do nothing, just consume the result set + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + break; + } + } + } + }); + // Wait until the entire result set has been consumed. + get(finished); + } + // increase the abort rate to 50% + interceptor.setProbability(0.50D); + connection.executeUpdateAsync( + Statement.newBuilder("UPDATE TEST SET NAME='updated' WHERE ID<@max_id") + .bind("max_id") + .to(UPDATED_RECORDS) + .build()); + // Wait for the commit to finish, as it could be that the transaction is aborted so many times + // that the last update does not succeed. + get(connection.commitAsync()); + // verify that the update succeeded + try (AsyncResultSet rs = + connection.executeQueryAsync( + Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE NAME='updated'"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong("C")).isEqualTo(UPDATED_RECORDS); + assertThat(rs.next()).isFalse(); + } + get(connection.commitAsync()); + } catch (AbortedException e) { + // This could happen if the number of aborts exceeds the max number of retries. + logger.log(Level.FINE, "testRetryHighAbortRate aborted because of too many retries", e); + } + logger.fine("Total number of retries started: " + RETRY_STATISTICS.totalRetryAttemptsStarted); + logger.fine("Total number of retries finished: " + RETRY_STATISTICS.totalRetryAttemptsFinished); + logger.fine("Total number of retries successful: " + RETRY_STATISTICS.totalSuccessfulRetries); + logger.fine("Total number of retries aborted: " + RETRY_STATISTICS.totalNestedAborts); + logger.fine( + "Total number of times the max retry count was exceeded: " + + RETRY_STATISTICS.totalMaxAttemptsExceeded); + } + + private void assertRetryStatistics( + int minAttemptsStartedExpected, + int concurrentModificationsExpected, + int successfulRetriesExpected) { + assertThat(RETRY_STATISTICS.totalRetryAttemptsStarted).isAtLeast(minAttemptsStartedExpected); + assertThat(RETRY_STATISTICS.totalConcurrentModifications) + .isEqualTo(concurrentModificationsExpected); + assertThat(RETRY_STATISTICS.totalSuccessfulRetries).isAtLeast(successfulRetriesExpected); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java index d6c89c65d93..899771b9e50 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java @@ -39,7 +39,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -53,7 +52,6 @@ @Category(ParallelIntegrationTest.class) @RunWith(JUnit4.class) public class ITReadOnlySpannerTest extends ITAbstractSpannerTest { - private static final Logger logger = Logger.getLogger(ITReadOnlySpannerTest.class.getName()); private static final long TEST_ROWS_COUNT = 1000L; @Override @@ -126,30 +124,21 @@ public void testStatementTimeoutTransactional() { @Test public void testStatementTimeoutTransactionalMultipleStatements() { - long startTime = System.currentTimeMillis(); try (ITConnection connection = createConnection()) { connection.beginTransaction(); for (int i = 0; i < 3; i++) { - boolean timedOut = false; - connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); + connection.setStatementTimeout(1L, TimeUnit.MICROSECONDS); try (ResultSet rs = connection.executeQuery( Statement.of( "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) { + fail("Missing expected exception"); } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; + assertThat(e.getErrorCode(), is(ErrorCode.DEADLINE_EXCEEDED)); } - assertThat(timedOut, is(true)); } connection.commit(); } - long endTime = System.currentTimeMillis(); - long executionTime = endTime - startTime; - if (executionTime > 25L) { - logger.warning("Total test execution time exceeded 25 milliseconds: " + executionTime); - } else { - logger.info("Total test execution time: " + executionTime); - } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java index a6b4fc88734..e8a479c6d63 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java @@ -186,6 +186,8 @@ public void test02_RunAbortedTest() { // verify that the commit aborted, an internal retry was started and then aborted because of // the concurrent modification assertThat(expectedException, is(true)); + // Rollback the transaction to start a new one. + connection.rollback(); // verify that the prices were changed try (ResultSet rs = connection.executeQuery( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java index c1567496bc1..1d7de23cb40 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java @@ -767,7 +767,8 @@ public void testAbortWithConcurrentInsertAndContinue() { } assertThat(expectedException, is(true)); assertRetryStatistics(1, 1, 0); - // the next statement should be in a new transaction as the previous transaction rolled back + // Rollback the aborted transaction to start a new one. + connection.rollback(); try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { // there should be one record from the transaction on connection2 assertThat(rs.next(), is(true)); @@ -1519,6 +1520,7 @@ public void testRetryHighAbortRate() { @Test public void testAbortWithConcurrentInsertOnEmptyTable() { assumeFalse("concurrent transactions are not supported on the emulator", isUsingEmulator()); + AbortInterceptor interceptor = new AbortInterceptor(0); try (ITConnection connection = createConnection(interceptor, new CountTransactionRetryListener())) { diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/connection/ITSqlScriptTest_TestStatementTimeout.sql b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/connection/ITSqlScriptTest_TestStatementTimeout.sql index 9a9894fafa9..7e8d907b953 100644 --- a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/connection/ITSqlScriptTest_TestStatementTimeout.sql +++ b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/connection/ITSqlScriptTest_TestStatementTimeout.sql @@ -70,7 +70,7 @@ SET STATEMENT_TIMEOUT='1ns'; SHOW VARIABLE STATEMENT_TIMEOUT; -- Do a somewhat complex query that should now timeout -@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED: Statement execution timeout occurred' +@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED:' SELECT COUNT(*) AS ACTUAL, 0 AS EXPECTED FROM ( SELECT * @@ -97,7 +97,7 @@ FROM ( ; -- Try to execute an update that should also timeout -@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED: Statement execution timeout occurred' +@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED:' UPDATE Singers SET LastName='Some Other Last Name' /* It used to be 'Last 1' */ WHERE SingerId=1 OR LastName IN ( @@ -176,7 +176,7 @@ SET STATEMENT_TIMEOUT='1ns'; SHOW VARIABLE STATEMENT_TIMEOUT; -- Do a somewhat complex query that should now timeout -@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED: Statement execution timeout occurred' +@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED:' SELECT COUNT(*) AS ACTUAL, 0 AS EXPECTED FROM ( SELECT * @@ -202,11 +202,17 @@ FROM ( ) RES ; -- We need to rollback the transaction as it is no longer usable. -@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED: Statement execution timeout occurred' +-- A timeout during a rollback is ignored, and also not rolling back +-- a transaction on the emulator will make the transaction remain the +-- current transaction. We therefore remove the timeout before the +-- rollback call. +SET STATEMENT_TIMEOUT=null; ROLLBACK; +SET STATEMENT_TIMEOUT='1ns'; + -- Try to execute an update that should also timeout -@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED: Statement execution timeout occurred' +@EXPECT EXCEPTION DEADLINE_EXCEEDED 'DEADLINE_EXCEEDED:' UPDATE Singers SET LastName='Some Other Last Name' /* It used to be 'Last 1' */ WHERE SingerId=1 OR LastName IN ( From eb5992949de326326a6bb02ec75b4a2a65a37b84 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 8 Oct 2020 19:02:07 +0200 Subject: [PATCH 42/79] deps: update dependency com.google.cloud:google-cloud-shared-dependencies to v0.10.2 (#500) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [com.google.cloud:google-cloud-shared-dependencies](https://togithub.com/googleapis/java-shared-dependencies) | patch | `0.10.0` -> `0.10.2` | --- ### Release Notes

    googleapis/java-shared-dependencies ### [`v0.10.2`](https://togithub.com/googleapis/java-shared-dependencies/blob/master/CHANGELOG.md#​0102-httpswwwgithubcomgoogleapisjava-shared-dependenciescompare0101v0102-2020-10-08) [Compare Source](https://togithub.com/googleapis/java-shared-dependencies/compare/v0.10.1...v0.10.2) ### [`v0.10.1`](https://togithub.com/googleapis/java-shared-dependencies/blob/master/CHANGELOG.md#​0101-httpswwwgithubcomgoogleapisjava-shared-dependenciescompare0100v0101-2020-10-06) [Compare Source](https://togithub.com/googleapis/java-shared-dependencies/compare/v0.10.0...v0.10.1)
    --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9db4299935c..c310c665ffd 100644 --- a/pom.xml +++ b/pom.xml @@ -106,7 +106,7 @@ com.google.cloud google-cloud-shared-dependencies - 0.10.0 + 0.10.2 pom import From 44882238f73e3659306e6c63aa652e7a0bc6e43f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 8 Oct 2020 23:44:12 +0200 Subject: [PATCH 43/79] chore(deps): update dependency com.google.cloud:google-cloud-spanner to v2.0.2 (#491) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [com.google.cloud:google-cloud-spanner](https://togithub.com/googleapis/java-spanner) | patch | `2.0.1` -> `2.0.2` | --- ### Release Notes
    googleapis/java-spanner ### [`v2.0.2`](https://togithub.com/googleapis/java-spanner/blob/master/CHANGELOG.md#​202-httpswwwgithubcomgoogleapisjava-spannercomparev201v202-2020-10-02) [Compare Source](https://togithub.com/googleapis/java-spanner/compare/v2.0.1...v2.0.2)
    --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/install-without-bom/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 4744333966b..0034dcf50c1 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -32,7 +32,7 @@ com.google.cloud google-cloud-spanner - 2.0.1 + 2.0.2 From 52a537664302be5d9255fc4932f2e927b407f0e6 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 8 Oct 2020 15:12:07 -0700 Subject: [PATCH 44/79] chore: regenerate README (#503) This PR was generated using Autosynth. :rainbow:
    Log from Synthtool ``` 2020-10-08 21:46:24,402 synthtool [DEBUG] > Executing /root/.cache/synthtool/java-spanner/.github/readme/synth.py. On branch autosynth-readme nothing to commit, working directory clean 2020-10-08 21:46:25,440 synthtool [DEBUG] > Wrote metadata to .github/readme/synth.metadata/synth.metadata. ```
    Full log will be available here: https://source.cloud.google.com/results/invocations/b7534e38-f656-4da4-8bac-c5738eaa214f/targets - [ ] To automatically regenerate this PR, check this box. --- .github/readme/synth.metadata/synth.metadata | 18 +++++++++++++ README.md | 28 ++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .github/readme/synth.metadata/synth.metadata diff --git a/.github/readme/synth.metadata/synth.metadata b/.github/readme/synth.metadata/synth.metadata new file mode 100644 index 00000000000..6cc05107a78 --- /dev/null +++ b/.github/readme/synth.metadata/synth.metadata @@ -0,0 +1,18 @@ +{ + "sources": [ + { + "git": { + "name": ".", + "remote": "https://github.com/googleapis/java-spanner.git", + "sha": "44882238f73e3659306e6c63aa652e7a0bc6e43f" + } + }, + { + "git": { + "name": "synthtool", + "remote": "https://github.com/googleapis/synthtool.git", + "sha": "b6164c26a111f7f587099d31253abb96b5737bb2" + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 5a6157bea33..461e4d77a99 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file com.google.cloud libraries-bom - 10.1.0 + 12.0.0 pom import @@ -38,7 +38,7 @@ If you are using Maven without BOM, add this to your dependencies: com.google.cloud google-cloud-spanner - 2.0.1 + 2.0.2 ``` @@ -213,6 +213,30 @@ dependency to collect the data and exporter dependency to export to backend. +## Samples + +Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/master/samples) directory. The samples' `README.md` +has instructions for running the samples. + +| Sample | Source Code | Try it | +| --------------------------- | --------------------------------- | ------ | +| Async Dml Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java) | +| Async Query Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncQueryExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncQueryExample.java) | +| Async Query To List Async Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncQueryToListAsyncExample.java) | +| Async Read Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncReadExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncReadExample.java) | +| Async Read Only Transaction Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncReadOnlyTransactionExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncReadOnlyTransactionExample.java) | +| Async Read Row Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncReadRowExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncReadRowExample.java) | +| Async Read Using Index Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncReadUsingIndexExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncReadUsingIndexExample.java) | +| Async Runner Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncRunnerExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncRunnerExample.java) | +| Async Transaction Manager Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/AsyncTransactionManagerExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncTransactionManagerExample.java) | +| Batch Sample | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/BatchSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/BatchSample.java) | +| Create Instance Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/CreateInstanceExample.java) | +| Custom Timeout And Retry Settings Example | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/CustomTimeoutAndRetrySettingsExample.java) | +| Quickstart Sample | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java) | +| Spanner Sample | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/SpannerSample.java) | +| Tracing Sample | [source code](https://github.com/googleapis/java-spanner/blob/master/samples/snippets/src/main/java/com/example/spanner/TracingSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/TracingSample.java) | + + ## Troubleshooting From 62fa39a2fbac6aa667073f16898e6861f0f5ec21 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 9 Oct 2020 04:47:55 +0200 Subject: [PATCH 45/79] deps: update opencensus.version to v0.27.1 (#497) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 0034dcf50c1..5d27eb4175d 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.26.0 + 0.27.1 1.1.0 1.100.1 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 8b6362d3111..1fc27825180 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.26.0 + 0.27.1 1.1.0 1.100.1 diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index f612cbf457d..72d351b1a64 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.26.0 + 0.27.1 From 208674632b20b37f51b828c1c4cc76c91154952b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 9 Oct 2020 04:49:03 +0200 Subject: [PATCH 46/79] fix: close executor when closing pool (#501) --- .../cloud/spanner/connection/SpannerPool.java | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index 350cf61394e..de351c87c93 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -398,26 +398,34 @@ void checkAndCloseSpanners(CheckAndCloseSpannersMode mode) { keysStillInUse.add(entry.getKey()); } } - if (keysStillInUse.isEmpty() || mode == CheckAndCloseSpannersMode.WARN) { - if (!keysStillInUse.isEmpty()) { + try { + if (keysStillInUse.isEmpty() || mode == CheckAndCloseSpannersMode.WARN) { + if (!keysStillInUse.isEmpty()) { + logLeakedConnections(keysStillInUse); + logger.log( + Level.WARNING, + "There is/are " + + keysStillInUse.size() + + " connection(s) still open." + + " Close all connections before stopping the application"); + } + // Force close all Spanner instances by passing in a value that will always be less than + // the + // difference between the current time and the close time of a connection. + closeUnusedSpanners(Long.MIN_VALUE); + } else { logLeakedConnections(keysStillInUse); - logger.log( - Level.WARNING, + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, "There is/are " + keysStillInUse.size() - + " connection(s) still open." - + " Close all connections before stopping the application"); + + " connection(s) still open. Close all connections before calling closeSpanner()"); } - // Force close all Spanner instances by passing in a value that will always be less than the - // difference between the current time and the close time of a connection. - closeUnusedSpanners(Long.MIN_VALUE); - } else { - logLeakedConnections(keysStillInUse); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "There is/are " - + keysStillInUse.size() - + " connection(s) still open. Close all connections before calling closeSpanner()"); + } finally { + if (closerService != null) { + closerService.shutdown(); + } + initialized = false; } } } From 3ab7348781e56384921d8287a5b5c0725dfed221 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 9 Oct 2020 06:25:48 +0200 Subject: [PATCH 47/79] deps: update dependency com.google.cloud:google-cloud-monitoring to v2 (#498) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 5d27eb4175d..ad2047be41d 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.27.1 1.1.0 - 1.100.1 + 2.0.4 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 1fc27825180..bb2a3d0dd38 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.27.1 1.1.0 - 1.100.1 + 2.0.4 From 3914577b643d96aa3d61de86251c0ab068c96938 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 12 Oct 2020 18:59:15 +0200 Subject: [PATCH 48/79] test(deps): update dependency junit:junit to v4.13.1 --- pom.xml | 2 +- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index c310c665ffd..1870eab5be0 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,7 @@ junit junit - 4.13 + 4.13.1 test diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index ad2047be41d..6bd6498b1a4 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -93,7 +93,7 @@ junit junit - 4.13 + 4.13.1 test diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index bb2a3d0dd38..7a22f8e19a1 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -92,7 +92,7 @@ junit junit - 4.13 + 4.13.1 test diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 72d351b1a64..1d7264e037a 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -102,7 +102,7 @@ junit junit - 4.13 + 4.13.1 test From c580df8e1175bde293890c2a68e8816951c068d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 14 Oct 2020 15:18:00 +0200 Subject: [PATCH 49/79] fix: AsyncTransactionManager should rollback on close (#505) * fix: AsyncTransactionManager should rollback on close AsyncTransctionManager should rollback the transaction if close is called while the transaction is still in state STARTED. Failing to do so, will keep the transaction open on the backend for longer than necessary, holding on to locks until it is garbage collected after 10 seconds. Fixes #504 * feat: return rollback result from close * fix: add ignored diff to clirr --- .../clirr-ignored-differences.xml | 6 ++++ .../spanner/AsyncTransactionManager.java | 10 ++++-- .../spanner/AsyncTransactionManagerImpl.java | 11 ++++++ .../SessionPoolAsyncTransactionManager.java | 34 ++++++++++++++++--- .../spanner/AsyncTransactionManagerTest.java | 28 +++++++++++++++ .../cloud/spanner/MockSpannerServiceImpl.java | 19 +++++++++++ 6 files changed, 101 insertions(+), 7 deletions(-) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index cfbcb88f852..1f7beb76e9a 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -371,4 +371,10 @@ com/google/cloud/spanner/ResultSets com.google.cloud.spanner.AsyncResultSet toAsyncResultSet(com.google.cloud.spanner.ResultSet, com.google.api.gax.core.ExecutorProvider) + + + 7012 + com/google/cloud/spanner/AsyncTransactionManager + com.google.api.core.ApiFuture closeAsync() + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java index d519c68013f..02d4a9dbd23 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java @@ -18,9 +18,6 @@ import com.google.api.core.ApiFuture; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionFunction; -import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; -import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -200,4 +197,11 @@ public interface AsyncTransactionFunction { */ @Override void close(); + + /** + * Closes the transaction manager. If there is an active transaction, it will be rolled back. The + * underlying session will be released back to the session pool. The returned {@link ApiFuture} is + * done when the transaction (if any) has been rolled back. + */ + ApiFuture closeAsync(); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index 082fa827e73..350349af163 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -24,6 +24,7 @@ import com.google.cloud.spanner.SessionImpl.SessionTransaction; import com.google.cloud.spanner.TransactionContextFutureImpl.CommittableAsyncTransactionManager; import com.google.cloud.spanner.TransactionManager.TransactionState; +import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; import io.opencensus.trace.Span; @@ -54,7 +55,17 @@ public void setSpan(Span span) { @Override public void close() { + closeAsync(); + } + + @Override + public ApiFuture closeAsync() { + ApiFuture res = null; + if (txnState == TransactionState.STARTED) { + res = rollbackAsync(); + } txn.close(); + return MoreObjects.firstNonNull(res, ApiFutures.immediateFuture(null)); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java index 55b6102a270..54b621b93b8 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java @@ -22,7 +22,6 @@ import com.google.api.core.ApiFutures; import com.google.api.core.SettableApiFuture; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.TransactionContextFutureImpl.CommittableAsyncTransactionManager; import com.google.cloud.spanner.TransactionManager.TransactionState; @@ -59,14 +58,41 @@ public void run() { @Override public void close() { - delegate.addListener( - new Runnable() { + SpannerApiFutures.get(closeAsync()); + } + + @Override + public ApiFuture closeAsync() { + final SettableApiFuture res = SettableApiFuture.create(); + ApiFutures.addCallback( + delegate, + new ApiFutureCallback() { @Override - public void run() { + public void onFailure(Throwable t) { session.close(); } + + @Override + public void onSuccess(AsyncTransactionManagerImpl result) { + ApiFutures.addCallback( + result.closeAsync(), + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + res.setException(t); + } + + @Override + public void onSuccess(Void result) { + session.close(); + res.set(result); + } + }, + MoreExecutors.directExecutor()); + } }, MoreExecutors.directExecutor()); + return res; } @Override diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java index c7b95f33f63..bf1f214a715 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java @@ -36,7 +36,9 @@ import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.Options.ReadOption; +import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.common.base.Function; +import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Range; @@ -47,6 +49,8 @@ import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.RollbackRequest; +import com.google.spanner.v1.TransactionSelector; import io.grpc.Status; import java.util.Arrays; import java.util.Collection; @@ -181,6 +185,30 @@ public void onSuccess(long[] input) { } } + @Test + public void asyncTransactionManager_shouldRollbackOnCloseAsync() throws Exception { + AsyncTransactionManager manager = client().transactionManagerAsync(); + TransactionContext txn = manager.beginAsync().get(); + txn.executeUpdateAsync(UPDATE_STATEMENT).get(); + final TransactionSelector selector = ((TransactionContextImpl) txn).getTransactionSelector(); + + SpannerApiFutures.get(manager.closeAsync()); + // The mock server should already have the Rollback request, as we are waiting for the returned + // ApiFuture to be done. + mockSpanner.waitForRequestsToContain( + new Predicate() { + @Override + public boolean apply(AbstractMessage input) { + if (input instanceof RollbackRequest) { + RollbackRequest request = (RollbackRequest) input; + return request.getTransactionId().equals(selector.getId()); + } + return false; + } + }, + 0L); + } + @Test public void asyncTransactionManagerUpdate() throws Exception { final SettableApiFuture updateCount = SettableApiFuture.create(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 4f55cd5ebd7..5ecf9607a49 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -23,8 +23,10 @@ import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; import com.google.common.base.Stopwatch; import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Uninterruptibles; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ByteString; @@ -1927,6 +1929,23 @@ public void waitForRequestsToContain(Class type, long } } + public void waitForRequestsToContain( + Predicate predicate, long timeoutMillis) + throws InterruptedException, TimeoutException { + Stopwatch watch = Stopwatch.createStarted(); + while (true) { + Iterable msg = Iterables.filter(getRequests(), predicate); + if (msg.iterator().hasNext()) { + break; + } + Thread.sleep(10L); + if (watch.elapsed(TimeUnit.MILLISECONDS) > timeoutMillis) { + throw new TimeoutException( + "Timeout while waiting for requests to contain the wanted request"); + } + } + } + @Override public void addResponse(AbstractMessage response) { throw new UnsupportedOperationException(); From 8fb6d4a49e18eb794f37aae88e2bbf1b3fd40a88 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 14 Oct 2020 09:16:22 -0700 Subject: [PATCH 50/79] ci(java): suggest formatting fixes (#511) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/d01b2aaa-4513-43a1-ac0a-689f57cc5d30/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/b65ef07d99946d23e900ef2cc490274a16edd336 --- .github/workflows/formatting.yaml | 25 +++++++++++++++++++++++++ synth.metadata | 5 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/formatting.yaml diff --git a/.github/workflows/formatting.yaml b/.github/workflows/formatting.yaml new file mode 100644 index 00000000000..d4d367cfce0 --- /dev/null +++ b/.github/workflows/formatting.yaml @@ -0,0 +1,25 @@ +on: + pull_request_target: + types: [opened, synchronize] + branches: + - master +name: format +jobs: + format-code: + runs-on: ubuntu-latest + env: + ACCESS_TOKEN: ${{ secrets.YOSHI_CODE_BOT_TOKEN }} + steps: + - uses: actions/checkout@v2 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + - uses: actions/setup-java@v1 + with: + java-version: 11 + - run: "mvn com.coveo:fmt-maven-plugin:format" + - uses: googleapis/code-suggester@v1.8.0 + with: + command: review + pull_number: ${{ github.event.pull_request.number }} + git_dir: '.' diff --git a/synth.metadata b/synth.metadata index d47379a450e..d2f0cc4d538 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "81b07e31a6a63a795d904094a72a6b51e593c314" + "sha": "3914577b643d96aa3d61de86251c0ab068c96938" } }, { @@ -19,7 +19,7 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "0762e8ee2ec21cdfc4d82020b985a104feb0453b" + "sha": "b65ef07d99946d23e900ef2cc490274a16edd336" } } ], @@ -63,6 +63,7 @@ ".github/trusted-contribution.yml", ".github/workflows/auto-release.yaml", ".github/workflows/ci.yaml", + ".github/workflows/formatting.yaml", ".github/workflows/samples.yaml", ".kokoro/build.bat", ".kokoro/build.sh", From 0595a80d5a6bb09e62ce1b6d101a3a039896c7af Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 15 Oct 2020 11:56:41 +0200 Subject: [PATCH 51/79] deps: update dependency com.google.cloud:google-cloud-trace to v1.2.3 (#496) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 6bd6498b1a4..4f34d163090 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.27.1 - 1.1.0 + 1.2.3 2.0.4 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 7a22f8e19a1..064a382f494 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.27.1 - 1.1.0 + 1.2.3 2.0.4 From 273f40c0b8b86aea1e1c9f54ca13723d4ce97298 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 16 Oct 2020 00:46:30 +0200 Subject: [PATCH 52/79] chore(deps): update dependency com.google.cloud:libraries-bom to v12.1.0 (#513) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [com.google.cloud:libraries-bom](https://togithub.com/GoogleCloudPlatform/cloud-opensource-java) | minor | `12.0.0` -> `12.1.0` | --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/snippets/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 1d7264e037a..524bc8d0593 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -31,7 +31,7 @@ com.google.cloud libraries-bom - 12.0.0 + 12.1.0 pom import From 4b8b8452589d63f6768b971a880a19bde80a9671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 16 Oct 2020 15:55:25 +0200 Subject: [PATCH 53/79] fix: AsyncTransactionManager did not propagate statement errors (#516) * fix: AsyncTransactionManager did not propagate statement errors Invalid statements or other statements that would cause an error would not cause the returned ApiFuture to fail. Fixes #514 * test: use existing invalid statement in test --- .../spanner/TransactionContextFutureImpl.java | 2 + .../spanner/AsyncTransactionManagerTest.java | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContextFutureImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContextFutureImpl.java index bc8262a5358..be21a947d1f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContextFutureImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContextFutureImpl.java @@ -121,6 +121,7 @@ public void onSuccess(I result) { @Override public void onFailure(Throwable t) { mgr.onError(t); + statementResult.setException(t); txnResult.setException(t); } @@ -132,6 +133,7 @@ public void onSuccess(O result) { MoreExecutors.directExecutor()); } catch (Throwable t) { mgr.onError(t); + statementResult.setException(t); txnResult.setException(t); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java index bf1f214a715..1d1b16d0148 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java @@ -60,6 +60,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; @@ -1112,4 +1113,48 @@ public ApiFuture apply(TransactionContext txn, Struct input) } } } + + @Test + public void asyncTransactionManager_shouldPropagateStatementFailure() + throws ExecutionException, InterruptedException, TimeoutException { + DatabaseClient dbClient = client(); + try (AsyncTransactionManager transactionManager = dbClient.transactionManagerAsync()) { + TransactionContextFuture txnContextFuture = transactionManager.beginAsync(); + AsyncTransactionStep updateFuture = + txnContextFuture.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) throws Exception { + return txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + } + }, + executor); + final SettableApiFuture res = SettableApiFuture.create(); + ApiFutures.addCallback( + updateFuture, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable throwable) { + // Check that we got the expected failure. + try { + assertThat(throwable).isInstanceOf(SpannerException.class); + SpannerException e = (SpannerException) throwable; + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(e.getMessage()).contains("invalid statement"); + res.set(null); + } catch (Throwable t) { + res.setException(t); + } + } + + @Override + public void onSuccess(Long aLong) { + res.setException(new AssertionError("Statement should not succeed.")); + } + }, + executor); + + assertThat(res.get(10L, TimeUnit.SECONDS)).isNull(); + } + } } From 5956677327e13ff23f44085feedff25188f1d4b5 Mon Sep 17 00:00:00 2001 From: Stephanie Wang Date: Fri, 16 Oct 2020 17:18:06 -0400 Subject: [PATCH 54/79] chore: add property tag for shared-deps (#520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-spanner/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # ☕️ --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1870eab5be0..435bc22060b 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,7 @@ UTF-8 github google-cloud-spanner-parent + 0.10.2 @@ -106,7 +107,7 @@ com.google.cloud google-cloud-shared-dependencies - 0.10.2 + ${google.cloud.shared-dependencies.version} pom import From 85086b4ec61b71290a3b436bad02232459480627 Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Mon, 19 Oct 2020 10:16:49 +1100 Subject: [PATCH 55/79] chore: Adds README for running the samples (#509) --- samples/README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 samples/README.md diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 00000000000..8f3cad92545 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,46 @@ +# Getting Started with Cloud Spanner and the Google Cloud Client libraries + + +Open in Cloud Shell + +[Cloud Spanner][Spanner] is a horizontally-scalable database-as-a-service +with transactions and SQL support. +These sample Java applications demonstrate how to access the Spanner API using +the [Google Cloud Client Library for Java][java-spanner]. + +[Spanner]: https://cloud.google.com/spanner/ +[java-spanner]: https://github.com/googleapis/java-spanner + +## Quickstart + +Every command here should be run from the root directory of your project (`java-spanner`). + +Install [Maven](http://maven.apache.org/). + +Build your project with: + + mvn clean package -DskipTests -DskipUTs -Penable-samples + +You can then run a given `ClassName` via: + + mvn exec:java -Dexec.mainClass=com.example.spanner.ClassName \ + -DpropertyName=propertyValue \ + -Dexec.args="any arguments to the app" + +### Running a simple query (using the quickstart sample) + + mvn exec:java -Dexec.mainClass=com.example.spanner.QuickstartSample -Dexec.args="my-instance my-database" + +## Tutorial + +### Running the tutorial + mvn exec:java -Dexec.mainClass=com.example.spanner.SpannerSample -Dexec.args=" my-instance my-database" + +## Tracing sample +`TracingSample.java` demonstrates how to export traces generated by client library to StackDriver and to /tracez page. + +### Running the tracing sample + mvn exec:java -Dexec.mainClass=com.example.spanner.TracingSample -Dexec.args="my-instance my-database" + +## Test + mvn verify -Dspanner.test.instance= -Dspanner.sample.database= -Dspanner.quickstart.database= From 0f13c4c5db37a736e391c002ed2456d78d04a090 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 19 Oct 2020 02:56:24 +0200 Subject: [PATCH 56/79] deps: update dependency org.openjdk.jmh:jmh-core to v1.26 (#506) --- google-cloud-spanner/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 7acd400ec7f..5cf248abb82 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -299,7 +299,7 @@ org.openjdk.jmh jmh-core - 1.25.2 + 1.26 test From 600f397a37f1808eb387fa3c31be0be5bb076c77 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 19 Oct 2020 04:50:24 +0200 Subject: [PATCH 57/79] deps: update dependency org.openjdk.jmh:jmh-generator-annprocess to v1.26 (#507) --- google-cloud-spanner/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 5cf248abb82..c88858f91cf 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -305,7 +305,7 @@ org.openjdk.jmh jmh-generator-annprocess - 1.25.2 + 1.26 test From 08ffb22c2b7e9cb76e3891fcac215cc197586976 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Sun, 18 Oct 2020 20:34:38 -0700 Subject: [PATCH 58/79] ci(java): restrict presubmit samples ITs (#517) Restrict presubmit sample ITs to only snapshot. This is to reduce resource consumption since we often times hit resource quota limit in samples testing. e.g. https://github.com/googleapis/java-bigquerydatatransfer/issues/410 Source-Author: Stephanie Wang Source-Date: Thu Oct 15 17:16:14 2020 -0400 Source-Repo: googleapis/synthtool Source-Sha: 27e0e916cbfdb3d5ff6639b686cc04f78a0b0386 Source-Link: https://github.com/googleapis/synthtool/commit/27e0e916cbfdb3d5ff6639b686cc04f78a0b0386 --- .kokoro/build.sh | 11 +++++++++-- synth.metadata | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 112a2cce858..32aa9311715 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -69,9 +69,16 @@ integration) RETURN_CODE=$? ;; samples) - if [[ -f samples/pom.xml ]] + SAMPLES_DIR=samples + # only run ITs in snapshot/ on presubmit PRs. run ITs in all 3 samples/ subdirectories otherwise. + if [[ ! -z ${KOKORO_GITHUB_PULL_REQUEST_NUMBER} ]] then - pushd samples + SAMPLES_DIR=samples/snapshot + fi + + if [[ -f ${SAMPLES_DIR}/pom.xml ]] + then + pushd {SAMPLES_DIR} mvn -B \ -Penable-samples \ -DtrimStackTrace=false \ diff --git a/synth.metadata b/synth.metadata index d2f0cc4d538..fd9bf6b7760 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "3914577b643d96aa3d61de86251c0ab068c96938" + "sha": "0595a80d5a6bb09e62ce1b6d101a3a039896c7af" } }, { @@ -19,7 +19,7 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "b65ef07d99946d23e900ef2cc490274a16edd336" + "sha": "27e0e916cbfdb3d5ff6639b686cc04f78a0b0386" } } ], From 4712a6bad401aca8d3c8d573c606fe0ec6afdb9a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 19 Oct 2020 22:13:39 +0200 Subject: [PATCH 59/79] build(deps): update dependency com.google.cloud:google-cloud-shared-config to v0.9.3 (#519) --- google-cloud-spanner-bom/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index e6e056633a3..589a6606387 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -8,7 +8,7 @@ com.google.cloud google-cloud-shared-config - 0.9.2 + 0.9.3 Google Cloud Spanner BOM diff --git a/pom.xml b/pom.xml index 435bc22060b..78835421590 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ com.google.cloud google-cloud-shared-config - 0.9.2 + 0.9.3 From a70c62e85bd51caf9602928e7445d39a27d3584e Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 19 Oct 2020 17:49:01 -0700 Subject: [PATCH 60/79] chore: regenerate README (#518) --- .github/readme/synth.metadata/synth.metadata | 4 ++-- README.md | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/readme/synth.metadata/synth.metadata b/.github/readme/synth.metadata/synth.metadata index 6cc05107a78..21966930532 100644 --- a/.github/readme/synth.metadata/synth.metadata +++ b/.github/readme/synth.metadata/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "44882238f73e3659306e6c63aa652e7a0bc6e43f" + "sha": "273f40c0b8b86aea1e1c9f54ca13723d4ce97298" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "b6164c26a111f7f587099d31253abb96b5737bb2" + "sha": "d5d03413e1879108b5ade8839ce38de01be652da" } } ] diff --git a/README.md b/README.md index 461e4d77a99..103bebf739e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file com.google.cloud libraries-bom - 12.0.0 + 12.1.0 pom import @@ -43,8 +43,6 @@ If you are using Maven without BOM, add this to your dependencies: ``` -[//]: # ({x-version-update-start:google-cloud-spanner:released}) - If you are using Gradle, add this to your dependencies ```Groovy compile 'com.google.cloud:google-cloud-spanner:2.0.2' @@ -53,7 +51,6 @@ If you are using SBT, add this to your dependencies ```Scala libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "2.0.2" ``` -[//]: # ({x-version-update-end}) ## Authentication From fb874ec2e1738d569d585d30825a6e9d3de96c66 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 20 Oct 2020 04:16:21 +0200 Subject: [PATCH 61/79] deps: update dependency com.google.cloud:google-cloud-monitoring to v2.0.5 (#525) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 4f34d163090..5424d305450 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.27.1 1.2.3 - 2.0.4 + 2.0.5 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 064a382f494..579a00d51c3 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.27.1 1.2.3 - 2.0.4 + 2.0.5 From 7d4c3805737b52a23c598c4c30ff7942d791ce55 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 19 Oct 2020 20:26:28 -0700 Subject: [PATCH 62/79] build(java): auto-approve README regeneration (#523) Source-Author: Jeff Ching Source-Date: Thu Oct 15 16:04:06 2020 -0700 Source-Repo: googleapis/synthtool Source-Sha: 7c5370937dd9ba9dcf9cd7d2af880a58b389b4f1 Source-Link: https://github.com/googleapis/synthtool/commit/7c5370937dd9ba9dcf9cd7d2af880a58b389b4f1 --- .github/workflows/approve-readme.yaml | 54 +++++++++++++++++++++++++++ synth.metadata | 5 ++- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/approve-readme.yaml diff --git a/.github/workflows/approve-readme.yaml b/.github/workflows/approve-readme.yaml new file mode 100644 index 00000000000..e2d841d6c5a --- /dev/null +++ b/.github/workflows/approve-readme.yaml @@ -0,0 +1,54 @@ +on: + pull_request: +name: auto-merge-readme +jobs: + approve: + runs-on: ubuntu-latest + if: github.repository_owner == 'googleapis' && github.head_ref == 'autosynth-readme' + steps: + - uses: actions/github-script@v3.0.0 + with: + github-token: ${{secrets.YOSHI_APPROVER_TOKEN}} + script: | + // only approve PRs from yoshi-automation + if (context.payload.pull_request.user.login !== "yoshi-automation") { + return; + } + + // only approve PRs like "chore: release " + if (!context.payload.pull_request.title === "chore: regenerate README") { + return; + } + + // only approve PRs with README.md and synth.metadata changes + const files = new Set( + ( + await github.paginate( + github.pulls.listFiles.endpoint({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }) + ) + ).map(file => file.filename) + ); + if (files.size != 2 || !files.has("README.md") || !files.has(".github/readme/synth.metadata/synth.metadata")) { + return; + } + + // approve README regeneration PR + await github.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Rubber stamped PR!', + pull_number: context.payload.pull_request.number, + event: 'APPROVE' + }); + + // attach automerge label + await github.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['automerge'] + }); diff --git a/synth.metadata b/synth.metadata index fd9bf6b7760..dd3976a92d0 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "0595a80d5a6bb09e62ce1b6d101a3a039896c7af" + "sha": "08ffb22c2b7e9cb76e3891fcac215cc197586976" } }, { @@ -19,7 +19,7 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "27e0e916cbfdb3d5ff6639b686cc04f78a0b0386" + "sha": "7c5370937dd9ba9dcf9cd7d2af880a58b389b4f1" } } ], @@ -61,6 +61,7 @@ ".github/readme/synth.py", ".github/release-please.yml", ".github/trusted-contribution.yml", + ".github/workflows/approve-readme.yaml", ".github/workflows/auto-release.yaml", ".github/workflows/ci.yaml", ".github/workflows/formatting.yaml", From 9c92cb48d3ef96bdb4af82d665f6ccaa4d5b30bf Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 20 Oct 2020 15:44:03 -0700 Subject: [PATCH 63/79] build(java): enable snippet-bot (#527) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/35a5efff-460c-411a-ad03-b70aaaa0fe6f/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/5a506ec8765cc04f7e29f888b8e9b257d9a7ae11 --- .github/snippet-bot.yml | 0 synth.metadata | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .github/snippet-bot.yml diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/synth.metadata b/synth.metadata index dd3976a92d0..bd91f6a9046 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "08ffb22c2b7e9cb76e3891fcac215cc197586976" + "sha": "7d4c3805737b52a23c598c4c30ff7942d791ce55" } }, { @@ -19,7 +19,7 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "7c5370937dd9ba9dcf9cd7d2af880a58b389b4f1" + "sha": "5a506ec8765cc04f7e29f888b8e9b257d9a7ae11" } } ], @@ -60,6 +60,7 @@ ".github/PULL_REQUEST_TEMPLATE.md", ".github/readme/synth.py", ".github/release-please.yml", + ".github/snippet-bot.yml", ".github/trusted-contribution.yml", ".github/workflows/approve-readme.yaml", ".github/workflows/auto-release.yaml", From 8d15a5801ab5693d917a6f46d908a5d9681ca02f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 21 Oct 2020 00:52:28 +0200 Subject: [PATCH 64/79] chore(deps): update dependency com.google.cloud:libraries-bom to v13 (#528) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [com.google.cloud:libraries-bom](https://togithub.com/GoogleCloudPlatform/cloud-opensource-java) | major | `12.1.0` -> `13.0.0` | --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/snippets/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 524bc8d0593..c0746ab5135 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -31,7 +31,7 @@ com.google.cloud libraries-bom - 12.1.0 + 13.0.0 pom import From 1138cd4e727e8ccc020d363635b582576c37dcd9 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 20 Oct 2020 16:38:09 -0700 Subject: [PATCH 65/79] chore: regenerate README (#530) This PR was generated using Autosynth. :rainbow:
    Log from Synthtool ``` 2020-10-20 23:01:18,465 synthtool [DEBUG] > Executing /root/.cache/synthtool/java-spanner/.github/readme/synth.py. On branch autosynth-readme nothing to commit, working directory clean 2020-10-20 23:01:19,448 synthtool [DEBUG] > Wrote metadata to .github/readme/synth.metadata/synth.metadata. ```
    Full log will be available here: https://source.cloud.google.com/results/invocations/bd2afeef-c881-4591-b19b-b940b0b5a699/targets - [ ] To automatically regenerate this PR, check this box. --- .github/readme/synth.metadata/synth.metadata | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/readme/synth.metadata/synth.metadata b/.github/readme/synth.metadata/synth.metadata index 21966930532..b61f520eac6 100644 --- a/.github/readme/synth.metadata/synth.metadata +++ b/.github/readme/synth.metadata/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "273f40c0b8b86aea1e1c9f54ca13723d4ce97298" + "sha": "8d15a5801ab5693d917a6f46d908a5d9681ca02f" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "d5d03413e1879108b5ade8839ce38de01be652da" + "sha": "6abb59097be84599a1d6091fe534a49e5c5cf948" } } ] diff --git a/README.md b/README.md index 103bebf739e..1cda4fe2790 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file com.google.cloud libraries-bom - 12.1.0 + 13.0.0 pom import From 1020989e1ec1ad7f5185579da58d7a839167f05a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 21 Oct 2020 03:15:24 +0200 Subject: [PATCH 66/79] deps: update dependency com.google.cloud:google-cloud-trace to v1.2.4 (#526) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 5424d305450..9b69cf05bf4 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.27.1 - 1.2.3 + 1.2.4 2.0.5 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 579a00d51c3..44da935938c 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.27.1 - 1.2.3 + 1.2.4 2.0.5 From 768c19dc1b9985f7823ec1e4ca92491936062f3b Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Wed, 21 Oct 2020 17:22:18 +1100 Subject: [PATCH 67/79] fix: fixes javadocs for Key (#532) Adds NUMERIC, TIMESTAMP and DATE types to the javadocs of the Key class. --- .../src/main/java/com/google/cloud/spanner/Key.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java index a8fc088b87e..cf9a839585b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java @@ -61,10 +61,13 @@ private Key(List parts) { * *
      *
    • {@code Boolean} for the {@code BOOL} Cloud Spanner type - *
    • {@code Integer}, {@code Long} for {@code INT64} - *
    • {@code Float}, {@code Double} for {@code FLOAT64} - *
    • {@code String} for {@code STRING} - *
    • {@link ByteArray} for {@code BYTES} + *
    • {@code Integer}, {@code Long} for the {@code INT64} Cloud Spanner type + *
    • {@code Float}, {@code Double} for the {@code FLOAT64} Cloud Spanner type + *
    • {@code BigDecimal} for the {@code NUMERIC} Cloud Spanner type + *
    • {@code String} for the {@code STRING} Cloud Spanner type + *
    • {@link ByteArray} for the {@code BYTES} Cloud Spanner type + *
    • {@link Timestamp} for the {@code TIMESTAMP} Cloud Spanner type + *
    • {@link Date} for the {@code DATE} Cloud Spanner type *
    * * @throws IllegalArgumentException if any member of {@code values} is not a supported type From c0a43bdc0401a37b3f34d9afd3cba413b84f0fe6 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 21 Oct 2020 11:16:30 -0700 Subject: [PATCH 68/79] chore: clean up and update renovate.json (#534) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/fc1413bf-7539-42ca-a3cb-6a8d29e26892/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/6abb59097be84599a1d6091fe534a49e5c5cf948 --- renovate.json | 30 +++++++++++------------------- synth.metadata | 4 ++-- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/renovate.json b/renovate.json index d0dceed0464..d03fbf5639d 100644 --- a/renovate.json +++ b/renovate.json @@ -17,24 +17,6 @@ ], "versionScheme": "docker" }, - { - "packagePatterns": [ - "^com.google.api:gax", - "^com.google.auth:", - "^com.google.cloud:google-cloud-core", - "^io.grpc:", - "^com.google.guava:" - ], - "groupName": "core dependencies" - }, - { - "packagePatterns": [ - "^com.google.http-client:", - "^com.google.oauth-client:", - "^com.google.api-client:" - ], - "groupName": "core transport dependencies" - }, { "packagePatterns": [ "*" @@ -63,6 +45,16 @@ "semanticCommitType": "chore", "semanticCommitScope": "deps" }, + { + "packagePatterns": [ + "^junit:junit", + "^com.google.truth:truth", + "^org.mockito:mockito-core", + "^org.objenesis:objenesis" + ], + "semanticCommitType": "test", + "semanticCommitScope": "deps" + }, { "packagePatterns": [ "^com.google.cloud:google-cloud-" @@ -78,4 +70,4 @@ ], "semanticCommits": true, "masterIssue": true -} \ No newline at end of file +} diff --git a/synth.metadata b/synth.metadata index bd91f6a9046..d1be22db27e 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "7d4c3805737b52a23c598c4c30ff7942d791ce55" + "sha": "768c19dc1b9985f7823ec1e4ca92491936062f3b" } }, { @@ -19,7 +19,7 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "5a506ec8765cc04f7e29f888b8e9b257d9a7ae11" + "sha": "6abb59097be84599a1d6091fe534a49e5c5cf948" } } ], From 35345acdd902b5c7be5d073349d6def6748eaf32 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 21 Oct 2020 20:32:24 +0200 Subject: [PATCH 69/79] chore(deps): update dependency com.google.cloud:libraries-bom to v13.1.0 (#535) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [com.google.cloud:libraries-bom](https://togithub.com/GoogleCloudPlatform/cloud-opensource-java) | minor | `13.0.0` -> `13.1.0` | --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/snippets/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index c0746ab5135..00df1d2f7d4 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -31,7 +31,7 @@ com.google.cloud libraries-bom - 13.0.0 + 13.1.0 pom import From 1811f52ba42eb380d927ac7451cb9634337adf96 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 21 Oct 2020 12:20:13 -0700 Subject: [PATCH 70/79] chore: regenerate README (#537) This PR was generated using Autosynth. :rainbow:
    Log from Synthtool ``` 2020-10-21 18:38:07,220 synthtool [DEBUG] > Executing /root/.cache/synthtool/java-spanner/.github/readme/synth.py. On branch autosynth-readme nothing to commit, working directory clean 2020-10-21 18:38:08,273 synthtool [DEBUG] > Wrote metadata to .github/readme/synth.metadata/synth.metadata. ```
    Full log will be available here: https://source.cloud.google.com/results/invocations/05805251-36ef-47fb-b1fb-c28f0ebb99c5/targets - [ ] To automatically regenerate this PR, check this box. --- .github/readme/synth.metadata/synth.metadata | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/readme/synth.metadata/synth.metadata b/.github/readme/synth.metadata/synth.metadata index b61f520eac6..6676f1f406e 100644 --- a/.github/readme/synth.metadata/synth.metadata +++ b/.github/readme/synth.metadata/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-spanner.git", - "sha": "8d15a5801ab5693d917a6f46d908a5d9681ca02f" + "sha": "35345acdd902b5c7be5d073349d6def6748eaf32" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "6abb59097be84599a1d6091fe534a49e5c5cf948" + "sha": "901ddd44e9ef7887ee681b9183bbdea99437fdcc" } } ] diff --git a/README.md b/README.md index 1cda4fe2790..23a232752ca 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file com.google.cloud libraries-bom - 13.0.0 + 13.1.0 pom import From 5c4c8c58674490ba524b678b409b8b19184af02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 22 Oct 2020 02:42:51 +0200 Subject: [PATCH 71/79] fix!: initialize should be protected (#536) * fix!: iniialize should be protected * fix: fix clirr build error --- .../clirr-ignored-differences.xml | 17 +++++++++++++++++ .../cloud/spanner/AbstractLazyInitializer.java | 2 +- .../cloud/spanner/LazySpannerInitializer.java | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 1f7beb76e9a..b68f50d78ea 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -377,4 +377,21 @@ com/google/cloud/spanner/AsyncTransactionManager com.google.api.core.ApiFuture closeAsync() + + + + 7009 + com/google/cloud/spanner/LazySpannerInitializer + com.google.cloud.spanner.Spanner initialize() + + + 7009 + com/google/cloud/spanner/LazySpannerInitializer + java.lang.Object initialize() + + + 7009 + com/google/cloud/spanner/AbstractLazyInitializer + java.lang.Object initialize() + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java index 0b1a4b3fc28..bc595b14662 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -51,5 +51,5 @@ public T get() throws Exception { * Initializes the actual object that should be returned. Is called once the first time an * instance of T is required. */ - public abstract T initialize() throws Exception; + protected abstract T initialize() throws Exception; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java index a157a5ac3ff..009c276751e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java @@ -23,7 +23,7 @@ public class LazySpannerInitializer extends AbstractLazyInitializer { * custom configuration. */ @Override - public Spanner initialize() throws Exception { + protected Spanner initialize() throws Exception { return SpannerOptions.newBuilder().build().getService(); } } From 777f5fc486de7a54801c9f3f82adca561388ebfe Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 22 Oct 2020 02:51:38 +0200 Subject: [PATCH 72/79] deps: update opencensus.version to v0.28.1 (#533) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 9b69cf05bf4..54e0cc89513 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.27.1 + 0.28.1 1.2.4 2.0.5 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 44da935938c..e8cefdd6f60 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.27.1 + 0.28.1 1.2.4 2.0.5 diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 00df1d2f7d4..c3465a18e27 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.27.1 + 0.28.1 From 5322c951ffaac38bc89c93ab510f312412fe2c53 Mon Sep 17 00:00:00 2001 From: skuruppu Date: Thu, 22 Oct 2020 20:23:33 +1100 Subject: [PATCH 73/79] test: remove explicit CreateInstance timeout (#524) Use the default settings for polling long-running operations instead. This change is made to prevent test flakiness from having too short of a timeout. --- .../test/java/com/google/cloud/spanner/IntegrationTestEnv.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java index b67f970273c..687bb44ba15 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java @@ -24,7 +24,6 @@ import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; import io.grpc.Status; import java.util.Random; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.junit.rules.ExternalResource; @@ -126,7 +125,7 @@ private void initializeInstance(InstanceId instanceId) { instanceAdminClient.createInstance(instance); Instance createdInstance; try { - createdInstance = op.get(30000L, TimeUnit.MILLISECONDS); + createdInstance = op.get(); } catch (Exception e) { boolean cancelled = false; try { From 659719deb5a18a87859bc174f5bde1e1147834d8 Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Fri, 23 Oct 2020 10:17:47 +1100 Subject: [PATCH 74/79] feat: adds options to the write operations (#531) * feat: adds options to the write operations Adds the possibility of adding options to the write operations and encapsulates the write response into it's own class so that we can augment the response with more fields than the commit timestamp. --- .../clirr-ignored-differences.xml | 12 ++++ .../google/cloud/spanner/CommitResponse.java | 52 ++++++++++++++++ .../google/cloud/spanner/DatabaseClient.java | 62 +++++++++++++++++++ .../cloud/spanner/DatabaseClientImpl.java | 15 +++++ .../com/google/cloud/spanner/Options.java | 3 + .../com/google/cloud/spanner/SessionImpl.java | 18 +++++- .../com/google/cloud/spanner/SessionPool.java | 30 ++++++++- 7 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index b68f50d78ea..3968118b1c7 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -378,6 +378,18 @@ com.google.api.core.ApiFuture closeAsync() + + + 7012 + com/google/cloud/spanner/DatabaseClient + com.google.cloud.spanner.CommitResponse writeWithOptions(java.lang.Iterable, com.google.cloud.spanner.Options$TransactionOption[]) + + + 7012 + com/google/cloud/spanner/DatabaseClient + com.google.cloud.spanner.CommitResponse writeAtLeastOnceWithOptions(java.lang.Iterable, com.google.cloud.spanner.Options$TransactionOption[]) + + 7009 diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java new file mode 100644 index 00000000000..dd5534d7c35 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.cloud.Timestamp; +import java.util.Objects; + +/** Represents a response from a commit operation. */ +public class CommitResponse { + + private final Timestamp commitTimestamp; + + public CommitResponse(Timestamp commitTimestamp) { + this.commitTimestamp = commitTimestamp; + } + + /** Returns a {@link Timestamp} representing the commit time of the write operation. */ + public Timestamp getCommitTimestamp() { + return commitTimestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CommitResponse that = (CommitResponse) o; + return Objects.equals(commitTimestamp, that.commitTimestamp); + } + + @Override + public int hashCode() { + return Objects.hash(commitTimestamp); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index d52d1d892e5..dc1f2a80c7f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Options.TransactionOption; /** * Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An @@ -52,6 +53,35 @@ public interface DatabaseClient { */ Timestamp write(Iterable mutations) throws SpannerException; + /** + * Writes the given mutations atomically to the database with the given options. + * + *

    This method uses retries and replay protection internally, which means that the mutations + * are applied exactly once on success, or not at all if an error is returned, regardless of any + * failures in the underlying network. Note that if the call is cancelled or reaches deadline, it + * is not possible to know whether the mutations were applied without performing a subsequent + * database operation, but the mutations will have been applied at most once. + * + *

    Example of blind write. + * + *

    {@code
    +   * long singerId = my_singer_id;
    +   * Mutation mutation = Mutation.newInsertBuilder("Singer")
    +   *         .set("SingerId")
    +   *         .to(singerId)
    +   *         .set("FirstName")
    +   *         .to("Billy")
    +   *         .set("LastName")
    +   *         .to("Joel")
    +   *         .build();
    +   * dbClient.writeWithOptions(Collections.singletonList(mutation));
    +   * }
    + * + * @return a response with the timestamp at which the write was committed + */ + CommitResponse writeWithOptions(Iterable mutations, TransactionOption... options) + throws SpannerException; + /** * Writes the given mutations atomically to the database without replay protection. * @@ -83,6 +113,38 @@ public interface DatabaseClient { */ Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException; + /** + * Writes the given mutations atomically to the database without replay protection. + * + *

    Since this method does not feature replay protection, it may attempt to apply {@code + * mutations} more than once; if the mutations are not idempotent, this may lead to a failure + * being reported when the mutation was applied once. For example, an insert may fail with {@link + * ErrorCode#ALREADY_EXISTS} even though the row did not exist before this method was called. For + * this reason, most users of the library will prefer to use {@link #write(Iterable)} instead. + * However, {@code writeAtLeastOnce()} requires only a single RPC, whereas {@code write()} + * requires two RPCs (one of which may be performed in advance), and so this method may be + * appropriate for latency sensitive and/or high throughput blind writing. + * + *

    Example of unprotected blind write. + * + *

    {@code
    +   * long singerId = my_singer_id;
    +   * Mutation mutation = Mutation.newInsertBuilder("Singers")
    +   *         .set("SingerId")
    +   *         .to(singerId)
    +   *         .set("FirstName")
    +   *         .to("Billy")
    +   *         .set("LastName")
    +   *         .to("Joel")
    +   *         .build();
    +   * dbClient.writeAtLeastOnce(Collections.singletonList(mutation));
    +   * }
    + * + * @return a response with the timestamp at which the write was committed + */ + CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException; + /** * Returns a context in which a single read can be performed using {@link TimestampBound#strong()} * concurrency. This method will return a {@link ReadContext} that will not return the read diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 4dd10001c70..f6af000d6ec 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.SpannerImpl.ClosedException; import com.google.common.annotations.VisibleForTesting; @@ -81,6 +82,13 @@ public Timestamp apply(Session session) { } } + @Override + public CommitResponse writeWithOptions(Iterable mutations, TransactionOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(final Iterable mutations) throws SpannerException { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); @@ -101,6 +109,13 @@ public Timestamp apply(Session session) { } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public ReadContext singleUse() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index 879b632d175..217d81b886e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -33,6 +33,9 @@ public interface ReadOption {} /** Marker interface to mark options applicable to query operation. */ public interface QueryOption {} + /** Marker interface to mark options applicable to write operations */ + public interface TransactionOption {} + /** Marker interface to mark options applicable to list operations in admin API. */ public interface ListOption {} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 6a91d85fef4..971dfc2ab1d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -25,6 +25,7 @@ import com.google.cloud.spanner.AbstractReadContext.MultiUseReadOnlyTransaction; import com.google.cloud.spanner.AbstractReadContext.SingleReadContext; import com.google.cloud.spanner.AbstractReadContext.SingleUseReadOnlyTransaction; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SessionClient.SessionId; import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; @@ -35,7 +36,6 @@ import com.google.protobuf.Empty; import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; -import com.google.spanner.v1.CommitResponse; import com.google.spanner.v1.Transaction; import com.google.spanner.v1.TransactionOptions; import io.opencensus.common.Scope; @@ -139,6 +139,13 @@ public Void run(TransactionContext ctx) { return runner.getCommitTimestamp(); } + @Override + public CommitResponse writeWithOptions(Iterable mutations, TransactionOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { setActive(null); @@ -154,7 +161,7 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx .build(); Span span = tracer.spanBuilder(SpannerImpl.COMMIT).startSpan(); try (Scope s = tracer.withSpan(span)) { - CommitResponse response = spanner.getRpc().commit(request, options); + com.google.spanner.v1.CommitResponse response = spanner.getRpc().commit(request, options); Timestamp t = Timestamp.fromProto(response.getCommitTimestamp()); return t; } catch (IllegalArgumentException e) { @@ -168,6 +175,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public ReadContext singleUse() { return singleUse(TimestampBound.strong()); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index 2512024117c..8d1e44fe9ad 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -47,10 +47,10 @@ import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.ReadOption; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.cloud.spanner.SpannerImpl.ClosedException; -import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.MoreObjects; @@ -1103,6 +1103,13 @@ public Timestamp write(Iterable mutations) throws SpannerException { } } + @Override + public CommitResponse writeWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { try { @@ -1112,6 +1119,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public ReadContext singleUse() { try { @@ -1347,6 +1361,13 @@ public Timestamp write(Iterable mutations) throws SpannerException { } } + @Override + public CommitResponse writeWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { try { @@ -1357,6 +1378,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public long executePartitionedUpdate(Statement stmt) throws SpannerException { try { From d08d3debb6457548bb6b04335b7a2d2227369211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 23 Oct 2020 02:39:51 +0200 Subject: [PATCH 75/79] feat: inline begin transaction (#325) * feat: inline begin tx with first statement * feat: support inlining BeginTransaction * fix: invalid dml statement can still return tx id * bench: add benchmarks for inline begin * feat: add inline begin for async runner * test: add additional tests and ITs * test: add tests for error during tx * test: use statement with same error code on emulator * test: skip test on emulator * test: constraint error causes transaction to be invalidated * fix: retry transaction if first statements fails and had BeginTransaction option * fix: handle aborted exceptions * test: add additional tests for corner cases * feat: use single-use tx for idem-potent mutations * fix: remove check for idempotent mutations * chore: remove commented code * feat!: remove session pool preparing (#515) * feat: remove session pool preparing * fix: fix integration tests * test: fix malformed retry loop in test case * fix: review comments * chore: run formatter * test: fix integration test that relied on data from other test case --- .../cloud/spanner/AbstractReadContext.java | 14 +- .../cloud/spanner/AbstractResultSet.java | 13 +- .../spanner/AsyncTransactionManagerImpl.java | 23 +- .../cloud/spanner/DatabaseClientImpl.java | 51 +- .../spanner/MetricRegistryConstants.java | 13 + .../com/google/cloud/spanner/SessionPool.java | 562 +------- .../cloud/spanner/SessionPoolOptions.java | 35 +- .../cloud/spanner/TransactionManagerImpl.java | 6 +- .../cloud/spanner/TransactionRunnerImpl.java | 403 +++--- .../google/cloud/spanner/AsyncRunnerTest.java | 31 +- .../spanner/AsyncTransactionManagerTest.java | 53 +- .../spanner/BatchCreateSessionsTest.java | 70 - .../cloud/spanner/DatabaseClientImplTest.java | 145 +-- .../cloud/spanner/GrpcResultSetTest.java | 8 +- .../spanner/ITSessionPoolIntegrationTest.java | 22 +- .../cloud/spanner/InlineBeginBenchmark.java | 264 ++++ .../spanner/InlineBeginTransactionTest.java | 1155 +++++++++++++++++ .../IntegrationTestWithClosedSessionsEnv.java | 18 +- .../cloud/spanner/MockSpannerServiceImpl.java | 29 +- .../cloud/spanner/ReadFormatTestRunner.java | 4 +- ...adWriteTransactionWithInlineBeginTest.java | 540 ++++++++ .../RetryOnInvalidatedSessionTest.java | 76 +- .../google/cloud/spanner/SessionImplTest.java | 5 + .../cloud/spanner/SessionPoolLeakTest.java | 27 +- .../spanner/SessionPoolMaintainerTest.java | 40 +- .../cloud/spanner/SessionPoolStressTest.java | 21 +- .../google/cloud/spanner/SessionPoolTest.java | 1013 ++++----------- .../com/google/cloud/spanner/SpanTest.java | 30 +- .../cloud/spanner/SpannerGaxRetryTest.java | 4 +- .../TransactionManagerAbortedTest.java | 16 +- .../spanner/TransactionManagerImplTest.java | 113 ++ .../spanner/TransactionRunnerImplTest.java | 74 +- .../cloud/spanner/it/ITClosedSessionTest.java | 22 +- .../google/cloud/spanner/it/ITDMLTest.java | 3 +- .../it/ITTransactionManagerAsyncTest.java | 18 +- .../spanner/it/ITTransactionManagerTest.java | 9 + .../cloud/spanner/it/ITTransactionTest.java | 128 +- 37 files changed, 3105 insertions(+), 1953 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java index f05fd10dda3..4ec5133b1aa 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java @@ -633,7 +633,8 @@ CloseableIterator startStream(@Nullable ByteString resumeToken return stream; } }; - return new GrpcResultSet(stream, this); + return new GrpcResultSet( + stream, this, request.hasTransaction() && request.getTransaction().hasBegin()); } /** @@ -672,14 +673,20 @@ public void close() { } } + /** + * Returns the {@link TransactionSelector} that should be used for a statement that is executed on + * this read context. This could be a reference to an existing transaction ID, or it could be a + * BeginTransaction option that should be included with the statement. + */ @Nullable abstract TransactionSelector getTransactionSelector(); + /** This method is called when a statement returned a new transaction as part of its results. */ @Override public void onTransactionMetadata(Transaction transaction) {} @Override - public void onError(SpannerException e) {} + public void onError(SpannerException e, boolean withBeginTransaction) {} @Override public void onDone() {} @@ -740,7 +747,8 @@ CloseableIterator startStream(@Nullable ByteString resumeToken return stream; } }; - GrpcResultSet resultSet = new GrpcResultSet(stream, this); + GrpcResultSet resultSet = + new GrpcResultSet(stream, this, selector != null && selector.hasBegin()); return resultSet; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 43bef07ce7a..3c5e60f51ae 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -81,7 +81,7 @@ interface Listener { void onTransactionMetadata(Transaction transaction) throws SpannerException; /** Called when the read finishes with an error. */ - void onError(SpannerException e); + void onError(SpannerException e, boolean withBeginTransaction); /** Called when the read finishes normally. */ void onDone(); @@ -91,14 +91,17 @@ interface Listener { static class GrpcResultSet extends AbstractResultSet> { private final GrpcValueIterator iterator; private final Listener listener; + private final boolean beginTransaction; private GrpcStruct currRow; private SpannerException error; private ResultSetStats statistics; private boolean closed; - GrpcResultSet(CloseableIterator iterator, Listener listener) { + GrpcResultSet( + CloseableIterator iterator, Listener listener, boolean beginTransaction) { this.iterator = new GrpcValueIterator(iterator); this.listener = listener; + this.beginTransaction = beginTransaction; } @Override @@ -127,7 +130,7 @@ public boolean next() throws SpannerException { } return hasNext; } catch (SpannerException e) { - throw yieldError(e); + throw yieldError(e, beginTransaction && currRow == null); } } @@ -149,9 +152,9 @@ public Type getType() { return currRow.getType(); } - private SpannerException yieldError(SpannerException e) { + private SpannerException yieldError(SpannerException e, boolean beginTransaction) { close(); - listener.onError(e); + listener.onError(e, beginTransaction); throw e; } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index 350349af163..2ba66d0c864 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.core.ApiAsyncFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; @@ -27,6 +28,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.Empty; import io.opencensus.trace.Span; import io.opencensus.trace.Tracer; import io.opencensus.trace.Tracing; @@ -76,14 +78,19 @@ public TransactionContextFutureImpl beginAsync() { return begin; } - private ApiFuture internalBeginAsync(boolean setActive) { + private ApiFuture internalBeginAsync(boolean firstAttempt) { txnState = TransactionState.STARTED; txn = session.newTransaction(); - if (setActive) { + if (firstAttempt) { session.setActive(this); } final SettableApiFuture res = SettableApiFuture.create(); - final ApiFuture fut = txn.ensureTxnAsync(); + final ApiFuture fut; + if (firstAttempt) { + fut = ApiFutures.immediateFuture(null); + } else { + fut = txn.ensureTxnAsync(); + } ApiFutures.addCallback( fut, new ApiFutureCallback() { @@ -149,7 +156,15 @@ public ApiFuture rollbackAsync() { txnState == TransactionState.STARTED, "rollback can only be called if the transaction is in progress"); try { - return txn.rollbackAsync(); + return ApiFutures.transformAsync( + txn.rollbackAsync(), + new ApiAsyncFunction() { + @Override + public ApiFuture apply(Empty input) throws Exception { + return ApiFutures.immediateFuture(null); + } + }, + MoreExecutors.directExecutor()); } finally { txnState = TransactionState.ROLLED_BACK; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index f6af000d6ec..a6dd90713b7 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -34,11 +34,6 @@ class DatabaseClientImpl implements DatabaseClient { private static final String PARTITION_DML_TRANSACTION = "CloudSpanner.PartitionDMLTransaction"; private static final Tracer tracer = Tracing.getTracer(); - private enum SessionMode { - READ, - READ_WRITE - } - @VisibleForTesting final String clientId; @VisibleForTesting final SessionPool pool; @@ -53,13 +48,8 @@ private enum SessionMode { } @VisibleForTesting - PooledSessionFuture getReadSession() { - return pool.getReadSession(); - } - - @VisibleForTesting - PooledSessionFuture getReadWriteSession() { - return pool.getReadWriteSession(); + PooledSessionFuture getSession() { + return pool.getSession(); } @Override @@ -67,7 +57,6 @@ public Timestamp write(final Iterable mutations) throws SpannerExcepti Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return runWithSessionRetry( - SessionMode.READ_WRITE, new Function() { @Override public Timestamp apply(Session session) { @@ -94,7 +83,6 @@ public Timestamp writeAtLeastOnce(final Iterable mutations) throws Spa Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return runWithSessionRetry( - SessionMode.READ_WRITE, new Function() { @Override public Timestamp apply(Session session) { @@ -120,7 +108,7 @@ public CommitResponse writeAtLeastOnceWithOptions( public ReadContext singleUse() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUse(); + return getSession().singleUse(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -131,7 +119,7 @@ public ReadContext singleUse() { public ReadContext singleUse(TimestampBound bound) { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUse(bound); + return getSession().singleUse(bound); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -142,7 +130,7 @@ public ReadContext singleUse(TimestampBound bound) { public ReadOnlyTransaction singleUseReadOnlyTransaction() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUseReadOnlyTransaction(); + return getSession().singleUseReadOnlyTransaction(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -153,7 +141,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction() { public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUseReadOnlyTransaction(bound); + return getSession().singleUseReadOnlyTransaction(bound); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -164,7 +152,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { public ReadOnlyTransaction readOnlyTransaction() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().readOnlyTransaction(); + return getSession().readOnlyTransaction(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -175,7 +163,7 @@ public ReadOnlyTransaction readOnlyTransaction() { public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().readOnlyTransaction(bound); + return getSession().readOnlyTransaction(bound); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -186,9 +174,9 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { public TransactionRunner readWriteTransaction() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().readWriteTransaction(); + return getSession().readWriteTransaction(); } catch (RuntimeException e) { - TraceUtil.setWithFailure(span, e); + TraceUtil.endSpanWithFailure(span, e); throw e; } finally { span.end(TraceUtil.END_SPAN_OPTIONS); @@ -199,7 +187,7 @@ public TransactionRunner readWriteTransaction() { public TransactionManager transactionManager() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().transactionManager(); + return getSession().transactionManager(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -210,7 +198,7 @@ public TransactionManager transactionManager() { public AsyncRunner runAsync() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().runAsync(); + return getSession().runAsync(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -221,7 +209,7 @@ public AsyncRunner runAsync() { public AsyncTransactionManager transactionManagerAsync() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().transactionManagerAsync(); + return getSession().transactionManagerAsync(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -232,10 +220,7 @@ public AsyncTransactionManager transactionManagerAsync() { public long executePartitionedUpdate(final Statement stmt) { Span span = tracer.spanBuilder(PARTITION_DML_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - // A partitioned update transaction does not need a prepared write session, as the transaction - // object will start a new transaction with specific options anyway. return runWithSessionRetry( - SessionMode.READ, new Function() { @Override public Long apply(Session session) { @@ -248,17 +233,13 @@ public Long apply(Session session) { } } - private T runWithSessionRetry(SessionMode mode, Function callable) { - PooledSessionFuture session = - mode == SessionMode.READ_WRITE ? getReadWriteSession() : getReadSession(); + private T runWithSessionRetry(Function callable) { + PooledSessionFuture session = getSession(); while (true) { try { return callable.apply(session); } catch (SessionNotFoundException e) { - session = - mode == SessionMode.READ_WRITE - ? pool.replaceReadWriteSession(e, session) - : pool.replaceReadSession(e, session); + session = pool.replaceSession(e, session); } } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java index 8da8ee15062..3512a75732d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java @@ -36,9 +36,22 @@ class MetricRegistryConstants { private static final LabelValue UNSET_LABEL = LabelValue.create(null); static final LabelValue NUM_IN_USE_SESSIONS = LabelValue.create("num_in_use_sessions"); + + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This metric will therefore always be zero and may be removed in the future. + */ + @Deprecated static final LabelValue NUM_SESSIONS_BEING_PREPARED = LabelValue.create("num_sessions_being_prepared"); + static final LabelValue NUM_READ_SESSIONS = LabelValue.create("num_read_sessions"); + + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This metric will therefore always be zero and may be removed in the future. + */ + @Deprecated static final LabelValue NUM_WRITE_SESSIONS = LabelValue.create("num_write_prepared_sessions"); static final ImmutableList SPANNER_LABEL_KEYS = diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index 8d1e44fe9ad..dbd82bbeb4e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -64,7 +64,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.protobuf.Empty; import io.opencensus.common.Scope; import io.opencensus.common.ToLongFunction; @@ -81,7 +80,6 @@ import io.opencensus.trace.Tracer; import io.opencensus.trace.Tracing; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -92,10 +90,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -109,10 +105,8 @@ import org.threeten.bp.Instant; /** - * Maintains a pool of sessions some of which might be prepared for write by invoking - * BeginTransaction rpc. It maintains two queues of sessions(read and write prepared) and two queues - * of waiters who are waiting for a session to become available. This class itself is thread safe - * and is meant to be used concurrently across multiple threads. + * Maintains a pool of sessions. This class itself is thread safe and is meant to be used + * concurrently across multiple threads. */ final class SessionPool { @@ -319,7 +313,7 @@ private void replaceSessionIfPossible(SessionNotFoundException notFound) { if (isSingleUse || !sessionUsedForQuery) { // This class is only used by read-only transactions, so we know that we only need a // read-only session. - session = sessionPool.replaceReadSession(notFound, session); + session = sessionPool.replaceSession(notFound, session); readContextDelegate = readContextDelegateSupplier.apply(session); } else { throw notFound; @@ -735,7 +729,7 @@ public TransactionContext begin() { try { return internalBegin(); } catch (SessionNotFoundException e) { - session = sessionPool.replaceReadWriteSession(e, session); + session = sessionPool.replaceSession(e, session); delegate = session.get().delegate.transactionManager(); } } @@ -748,7 +742,7 @@ private TransactionContext internalBegin() { } private SpannerException handleSessionNotFound(SessionNotFoundException notFound) { - session = sessionPool.replaceReadWriteSession(notFound, session); + session = sessionPool.replaceSession(notFound, session); delegate = session.get().delegate.transactionManager(); restartedAfterSessionNotFound = true; return SpannerExceptionFactory.newSpannerException( @@ -789,7 +783,7 @@ public TransactionContext resetForRetry() { return new SessionPoolTransactionContext(delegate.resetForRetry()); } } catch (SessionNotFoundException e) { - session = sessionPool.replaceReadWriteSession(e, session); + session = sessionPool.replaceSession(e, session); delegate = session.get().delegate.transactionManager(); restartedAfterSessionNotFound = true; } @@ -828,7 +822,7 @@ public TransactionState getState() { /** * {@link TransactionRunner} that automatically handles {@link SessionNotFoundException}s by - * replacing the underlying read/write session and then restarts the transaction. + * replacing the underlying session and then restarts the transaction. */ private static final class SessionPoolTransactionRunner implements TransactionRunner { private final SessionPool sessionPool; @@ -857,7 +851,7 @@ public T run(TransactionCallable callable) { result = getRunner().run(callable); break; } catch (SessionNotFoundException e) { - session = sessionPool.replaceReadWriteSession(e, session); + session = sessionPool.replaceSession(e, session); runner = session.get().delegate.readWriteTransaction(); } } @@ -915,8 +909,7 @@ public void run() { se = SpannerExceptionFactory.newSpannerException(t); } finally { if (se != null && se instanceof SessionNotFoundException) { - session = - sessionPool.replaceReadWriteSession((SessionNotFoundException) se, session); + session = sessionPool.replaceSession((SessionNotFoundException) se, session); } else { break; } @@ -965,109 +958,6 @@ private enum SessionState { CLOSING, } - /** - * Forwarding future that will return a {@link PooledSession}. If {@link #inProcessPrepare} has - * been set to true, the returned session will be prepared with a read/write session using the - * thread of the caller to {@link #get()}. This ensures that the executor that is responsible for - * background preparing of read/write transactions is not overwhelmed by requests in case of a - * large burst of write requests. Instead of filling up the queue of the background executor, the - * caller threads will be used for the BeginTransaction call. - */ - private final class ForwardingListenablePooledSessionFuture - extends SimpleForwardingListenableFuture { - private final boolean inProcessPrepare; - private final Span span; - private volatile boolean initialized = false; - private final Object prepareLock = new Object(); - private volatile PooledSession result; - private volatile SpannerException error; - - private ForwardingListenablePooledSessionFuture( - ListenableFuture delegate, boolean inProcessPrepare, Span span) { - super(delegate); - this.inProcessPrepare = inProcessPrepare; - this.span = span; - } - - @Override - public PooledSession get() throws InterruptedException, ExecutionException { - try { - return initialize(super.get()); - } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause()); - } catch (InterruptedException e) { - throw SpannerExceptionFactory.propagateInterrupt(e); - } - } - - @Override - public PooledSession get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - try { - return initialize(super.get(timeout, unit)); - } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause()); - } catch (InterruptedException e) { - throw SpannerExceptionFactory.propagateInterrupt(e); - } catch (TimeoutException e) { - throw SpannerExceptionFactory.propagateTimeout(e); - } - } - - private PooledSession initialize(PooledSession sess) { - if (!initialized) { - synchronized (prepareLock) { - if (!initialized) { - try { - result = prepare(sess); - } catch (Throwable t) { - error = SpannerExceptionFactory.newSpannerException(t); - } finally { - initialized = true; - } - } - } - } - if (error != null) { - throw error; - } - return result; - } - - private PooledSession prepare(PooledSession sess) { - if (inProcessPrepare && !sess.delegate.hasReadyTransaction()) { - while (true) { - try { - sess.prepareReadWriteTransaction(); - synchronized (lock) { - stopAutomaticPrepare = false; - } - break; - } catch (Throwable t) { - if (isClosed()) { - span.addAnnotation("Pool has been closed"); - throw new IllegalStateException("Pool has been closed"); - } - SpannerException e = newSpannerException(t); - WaiterFuture waiter = new WaiterFuture(); - synchronized (lock) { - handlePrepareSessionFailure(e, sess, false); - if (!isSessionNotFound(e)) { - throw e; - } - readWaiters.add(waiter); - } - sess = waiter.get(); - if (sess.delegate.hasReadyTransaction()) { - break; - } - } - } - } - return sess; - } - } - private PooledSessionFuture createPooledSessionFuture( ListenableFuture future, Span span) { return new PooledSessionFuture(future, span); @@ -1662,18 +1552,15 @@ private void removeIdleSessions(Instant currTime) { synchronized (lock) { // Determine the minimum last use time for a session to be deemed to still be alive. Remove // all sessions that have a lastUseTime before that time, unless it would cause us to go - // below MinSessions. Prefer to remove read sessions above write-prepared sessions. + // below MinSessions. Instant minLastUseTime = currTime.minus(options.getRemoveInactiveSessionAfter()); - for (Iterator iterator : - Arrays.asList( - readSessions.descendingIterator(), writePreparedSessions.descendingIterator())) { - while (iterator.hasNext()) { - PooledSession session = iterator.next(); - if (session.lastUseTime.isBefore(minLastUseTime)) { - if (session.state != SessionState.CLOSING) { - removeFromPool(session); - iterator.remove(); - } + Iterator iterator = sessions.descendingIterator(); + while (iterator.hasNext()) { + PooledSession session = iterator.next(); + if (session.lastUseTime.isBefore(minLastUseTime)) { + if (session.state != SessionState.CLOSING) { + removeFromPool(session); + iterator.remove(); } } } @@ -1703,12 +1590,7 @@ private void keepAliveSessions(Instant currTime) { while (numSessionsToKeepAlive > 0) { PooledSession sessionToKeepAlive = null; synchronized (lock) { - sessionToKeepAlive = findSessionToKeepAlive(readSessions, keepAliveThreshold, 0); - if (sessionToKeepAlive == null) { - sessionToKeepAlive = - findSessionToKeepAlive( - writePreparedSessions, keepAliveThreshold, readSessions.size()); - } + sessionToKeepAlive = findSessionToKeepAlive(sessions, keepAliveThreshold, 0); } if (sessionToKeepAlive == null) { break; @@ -1744,9 +1626,7 @@ private static enum Position { private final SessionClient sessionClient; private final ScheduledExecutorService executor; private final ExecutorFactory executorFactory; - private final ScheduledExecutorService prepareExecutor; - private final int prepareThreadPoolSize; final PoolMaintainer poolMaintainer; private final Clock clock; private final Object lock = new Object(); @@ -1768,19 +1648,10 @@ private static enum Position { private boolean stopAutomaticPrepare; @GuardedBy("lock") - private final LinkedList readSessions = new LinkedList<>(); - - @GuardedBy("lock") - private final LinkedList writePreparedSessions = new LinkedList<>(); - - @GuardedBy("lock") - private final Queue readWaiters = new LinkedList<>(); + private final LinkedList sessions = new LinkedList<>(); @GuardedBy("lock") - private final Queue readWriteWaiters = new LinkedList<>(); - - @GuardedBy("lock") - private int numSessionsBeingPrepared = 0; + private final Queue waiters = new LinkedList<>(); @GuardedBy("lock") private int numSessionsBeingCreated = 0; @@ -1797,12 +1668,6 @@ private static enum Position { @GuardedBy("lock") private long numSessionsReleased = 0; - @GuardedBy("lock") - private long numSessionsInProcessPrepared = 0; - - @GuardedBy("lock") - private long numSessionsAsyncPrepared = 0; - @GuardedBy("lock") private long numIdleSessionsRemoved = 0; @@ -1887,18 +1752,6 @@ private SessionPool( this.options = options; this.executorFactory = executorFactory; this.executor = executor; - if (executor instanceof ThreadPoolExecutor) { - prepareThreadPoolSize = Math.max(((ThreadPoolExecutor) executor).getCorePoolSize(), 1); - } else { - prepareThreadPoolSize = 8; - } - this.prepareExecutor = - Executors.newScheduledThreadPool( - prepareThreadPoolSize, - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("session-pool-prepare-%d") - .build()); this.sessionClient = sessionClient; this.clock = clock; this.poolMaintainer = new PoolMaintainer(); @@ -1912,19 +1765,6 @@ int getNumberOfSessionsInUse() { } } - long getNumberOfSessionsInProcessPrepared() { - synchronized (lock) { - return numSessionsInProcessPrepared; - } - } - - @VisibleForTesting - long getNumberOfSessionsAsyncPrepared() { - synchronized (lock) { - return numSessionsAsyncPrepared; - } - } - void removeFromPool(PooledSession session) { synchronized (lock) { if (isClosed()) { @@ -1946,24 +1786,10 @@ long numIdleSessionsRemoved() { } } - @VisibleForTesting - int getNumberOfAvailableWritePreparedSessions() { - synchronized (lock) { - return writePreparedSessions.size(); - } - } - @VisibleForTesting int getNumberOfSessionsInPool() { synchronized (lock) { - return readSessions.size() + writePreparedSessions.size() + numSessionsBeingPrepared; - } - } - - @VisibleForTesting - int getNumberOfWriteSessionsInPool() { - synchronized (lock) { - return writePreparedSessions.size() + numSessionsBeingPrepared; + return sessions.size(); } } @@ -1974,13 +1800,6 @@ int getNumberOfSessionsBeingCreated() { } } - @VisibleForTesting - int getNumberOfSessionsBeingPrepared() { - synchronized (lock) { - return numSessionsBeingPrepared; - } - } - @VisibleForTesting long getNumWaiterTimeouts() { return numWaiterTimeouts.get(); @@ -2017,11 +1836,6 @@ private boolean isDatabaseOrInstanceNotFound(SpannerException e) { return e instanceof DatabaseNotFoundException || e instanceof InstanceNotFoundException; } - private boolean shouldStopPrepareSessions(SpannerException e) { - return isDatabaseOrInstanceNotFound(e) - || SHOULD_STOP_PREPARE_SESSIONS_ERROR_CODES.contains(e.getErrorCode()); - } - private void invalidateSession(PooledSession session) { synchronized (lock) { if (isClosed()) { @@ -2059,8 +1873,8 @@ boolean isValid() { } /** - * Returns a session to be used for read requests to spanner. It will block if a session is not - * currently available. In case the pool is exhausted and {@link + * Returns a session to be used for requests to spanner. This method is always non-blocking and + * returns a {@link PooledSessionFuture}. In case the pool is exhausted and {@link * SessionPoolOptions#isFailIfPoolExhausted()} has been set, it will throw an exception. Returned * session must be closed by calling {@link Session#close()}. * @@ -2068,13 +1882,12 @@ boolean isValid() { * *
      *
    1. If a read session is available, return that. - *
    2. Otherwise if a writePreparedSession is available, return that. *
    3. Otherwise if a session can be created, fire a creation request. *
    4. Wait for a session to become available. Note that this can be unblocked either by a * session being returned to the pool or a new session being created. *
    */ - PooledSessionFuture getReadSession() throws SpannerException { + PooledSessionFuture getSession() throws SpannerException { Span span = Tracing.getTracer().getCurrentSpan(); span.addAnnotation("Acquiring session"); WaiterFuture waiter = null; @@ -2093,151 +1906,39 @@ PooledSessionFuture getReadSession() throws SpannerException { resourceNotFoundException.getMessage()), resourceNotFoundException); } - sess = readSessions.poll(); + sess = sessions.poll(); if (sess == null) { - sess = writePreparedSessions.poll(); - if (sess == null) { - span.addAnnotation("No session available"); - maybeCreateSession(); - waiter = new WaiterFuture(); - readWaiters.add(waiter); - } else { - span.addAnnotation("Acquired read write session"); - } + span.addAnnotation("No session available"); + maybeCreateSession(); + waiter = new WaiterFuture(); + waiters.add(waiter); } else { - span.addAnnotation("Acquired read only session"); + span.addAnnotation("Acquired session"); } - return checkoutSession(span, sess, waiter, false, false); - } - } - - /** - * Returns a session which has been prepared for writes by invoking BeginTransaction rpc. It will - * block if such a session is not currently available.In case the pool is exhausted and {@link - * SessionPoolOptions#isFailIfPoolExhausted()} has been set, it will throw an exception. Returned - * session must closed by invoking {@link Session#close()}. - * - *

    Implementation strategy: - * - *

      - *
    1. If a writePreparedSession is available, return that. - *
    2. Otherwise if we have an extra session being prepared for write, wait for that. - *
    3. Otherwise, if there is a read session available, start preparing that for write and wait. - *
    4. Otherwise start creating a new session and wait. - *
    5. Wait for write prepared session to become available. This can be unblocked either by the - * session create/prepare request we fired in above request or by a session being released - * to the pool which is then write prepared. - *
    - */ - PooledSessionFuture getReadWriteSession() { - Span span = Tracing.getTracer().getCurrentSpan(); - span.addAnnotation("Acquiring read write session"); - PooledSession sess = null; - WaiterFuture waiter = null; - boolean inProcessPrepare = stopAutomaticPrepare; - synchronized (lock) { - if (closureFuture != null) { - span.addAnnotation("Pool has been closed"); - throw new IllegalStateException("Pool has been closed", closedException); - } - if (resourceNotFoundException != null) { - span.addAnnotation("Database has been deleted"); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.NOT_FOUND, - String.format( - "The session pool has been invalidated because a previous RPC returned 'Database not found': %s", - resourceNotFoundException.getMessage()), - resourceNotFoundException); - } - sess = writePreparedSessions.poll(); - if (sess == null) { - if (!inProcessPrepare && numSessionsBeingPrepared <= prepareThreadPoolSize) { - if (numSessionsBeingPrepared <= readWriteWaiters.size()) { - PooledSession readSession = readSessions.poll(); - if (readSession != null) { - span.addAnnotation( - "Acquired read only session. Preparing for read write transaction"); - prepareSession(readSession); - } else { - span.addAnnotation("No session available"); - maybeCreateSession(); - } - } - } else { - inProcessPrepare = true; - numSessionsInProcessPrepared++; - PooledSession readSession = readSessions.poll(); - if (readSession != null) { - // Create a read/write transaction in-process if there is already a queue for prepared - // sessions. This is more efficient than doing it asynchronously, as it scales with - // the number of user threads. The thread pool for asynchronously preparing sessions - // is fixed. - span.addAnnotation( - "Acquired read only session. Preparing in-process for read write transaction"); - sess = readSession; - } else { - span.addAnnotation("No session available"); - maybeCreateSession(); - } - } - if (sess == null) { - waiter = new WaiterFuture(); - if (inProcessPrepare) { - // inProcessPrepare=true means that we have already determined that the queue for - // preparing read/write sessions is larger than the number of threads in the prepare - // thread pool, and that it's more efficient to do the prepare in-process. We will - // therefore create a waiter for a read-only session, even though a read/write session - // has been requested. - readWaiters.add(waiter); - } else { - readWriteWaiters.add(waiter); - } - } - } else { - span.addAnnotation("Acquired read write session"); - } - return checkoutSession(span, sess, waiter, true, inProcessPrepare); + return checkoutSession(span, sess, waiter); } } private PooledSessionFuture checkoutSession( - final Span span, - final PooledSession readySession, - WaiterFuture waiter, - boolean write, - final boolean inProcessPrepare) { + final Span span, final PooledSession readySession, WaiterFuture waiter) { ListenableFuture sessionFuture; if (waiter != null) { logger.log( Level.FINE, "No session available in the pool. Blocking for one to become available/created"); - span.addAnnotation( - String.format( - "Waiting for %s session to be available", write ? "read write" : "read only")); + span.addAnnotation(String.format("Waiting for a session to come available")); sessionFuture = waiter; } else { SettableFuture fut = SettableFuture.create(); fut.set(readySession); sessionFuture = fut; } - ForwardingListenablePooledSessionFuture forwardingFuture = - new ForwardingListenablePooledSessionFuture(sessionFuture, inProcessPrepare, span); - PooledSessionFuture res = createPooledSessionFuture(forwardingFuture, span); + PooledSessionFuture res = createPooledSessionFuture(sessionFuture, span); res.markCheckedOut(); return res; } - PooledSessionFuture replaceReadSession(SessionNotFoundException e, PooledSessionFuture session) { - return replaceSession(e, session, false); - } - - PooledSessionFuture replaceReadWriteSession( - SessionNotFoundException e, PooledSessionFuture session) { - return replaceSession(e, session, true); - } - - private PooledSessionFuture replaceSession( - SessionNotFoundException e, PooledSessionFuture session, boolean write) { + PooledSessionFuture replaceSession(SessionNotFoundException e, PooledSessionFuture session) { if (!options.isFailIfSessionNotFound() && session.get().allowReplacing) { synchronized (lock) { numSessionsInUse--; @@ -2246,7 +1947,7 @@ private PooledSessionFuture replaceSession( } session.leakedException = null; invalidateSession(session.get()); - return write ? getReadWriteSession() : getReadSession(); + return getSession(); } else { throw e; } @@ -2286,47 +1987,29 @@ private void maybeCreateSession() { } } } - /** - * Releases a session back to the pool. This might cause one of the waiters to be unblocked. - * - *

    Implementation note: - * - *

      - *
    1. If there are no pending waiters, either add to the read sessions queue or start preparing - * for write depending on what fraction of sessions are already prepared for writes. - *
    2. Otherwise either unblock a waiting reader or start preparing for a write. Exact strategy - * on which option we chose, in case there are both waiting readers and writers, is - * implemented in {@link #shouldUnblockReader} - *
    - */ + /** Releases a session back to the pool. This might cause one of the waiters to be unblocked. */ private void releaseSession(PooledSession session, Position position) { Preconditions.checkNotNull(session); synchronized (lock) { if (closureFuture != null) { return; } - if (readWaiters.size() == 0 && numSessionsBeingPrepared >= readWriteWaiters.size()) { + if (waiters.size() == 0) { // No pending waiters - if (shouldPrepareSession()) { - prepareSession(session); - } else { - switch (position) { - case RANDOM: - if (!readSessions.isEmpty()) { - int pos = random.nextInt(readSessions.size() + 1); - readSessions.add(pos, session); - break; - } - // fallthrough - case FIRST: - default: - readSessions.addFirst(session); - } + switch (position) { + case RANDOM: + if (!sessions.isEmpty()) { + int pos = random.nextInt(sessions.size() + 1); + sessions.add(pos, session); + break; + } + // fallthrough + case FIRST: + default: + sessions.addFirst(session); } - } else if (shouldUnblockReader()) { - readWaiters.poll().put(session); } else { - prepareSession(session); + waiters.poll().put(session); } } } @@ -2334,10 +2017,8 @@ private void releaseSession(PooledSession session, Position position) { private void handleCreateSessionsFailure(SpannerException e, int count) { synchronized (lock) { for (int i = 0; i < count; i++) { - if (readWaiters.size() > 0) { - readWaiters.poll().put(e); - } else if (readWriteWaiters.size() > 0) { - readWriteWaiters.poll().put(e); + if (waiters.size() > 0) { + waiters.poll().put(e); } else { break; } @@ -2348,42 +2029,6 @@ private void handleCreateSessionsFailure(SpannerException e, int count) { } } - private void handlePrepareSessionFailure( - SpannerException e, PooledSession session, boolean informFirstWaiter) { - synchronized (lock) { - if (isSessionNotFound(e)) { - invalidateSession(session); - } else if (shouldStopPrepareSessions(e)) { - // Database has been deleted or the user has no permission to write to this database, or - // there is some other semi-permanent error. We should stop trying to prepare any - // transactions. Also propagate the error to all waiters if the database or instance has - // been deleted, as any further waiting is pointless. - stopAutomaticPrepare = true; - while (readWriteWaiters.size() > 0) { - readWriteWaiters.poll().put(e); - } - while (readWaiters.size() > 0) { - readWaiters.poll().put(e); - } - if (isDatabaseOrInstanceNotFound(e)) { - // Remove the session from the pool. - if (isClosed()) { - decrementPendingClosures(1); - } - allSessions.remove(session); - setResourceNotFoundException((ResourceNotFoundException) e); - } else { - releaseSession(session, Position.FIRST); - } - } else if (informFirstWaiter && readWriteWaiters.size() > 0) { - releaseSession(session, Position.FIRST); - readWriteWaiters.poll().put(e); - } else { - releaseSession(session, Position.FIRST); - } - } - } - void setResourceNotFoundException(ResourceNotFoundException e) { this.resourceNotFoundException = MoreObjects.firstNonNull(this.resourceNotFoundException, e); } @@ -2396,9 +2041,9 @@ private void decrementPendingClosures(int count) { } /** - * Close all the sessions. Once this method is invoked {@link #getReadSession()} and {@link - * #getReadWriteSession()} will start throwing {@code IllegalStateException}. The returned future - * blocks till all the sessions created in this pool have been closed. + * Close all the sessions. Once this method is invoked {@link #getSession()} will start throwing + * {@code IllegalStateException}. The returned future blocks till all the sessions created in this + * pool have been closed. */ ListenableFuture closeAsync(ClosedException closedException) { ListenableFuture retFuture = null; @@ -2408,40 +2053,18 @@ ListenableFuture closeAsync(ClosedException closedException) { } this.closedException = closedException; // Fail all pending waiters. - WaiterFuture waiter = readWaiters.poll(); - while (waiter != null) { - waiter.put(newSpannerException(ErrorCode.INTERNAL, "Client has been closed")); - waiter = readWaiters.poll(); - } - waiter = readWriteWaiters.poll(); + WaiterFuture waiter = waiters.poll(); while (waiter != null) { waiter.put(newSpannerException(ErrorCode.INTERNAL, "Client has been closed")); - waiter = readWriteWaiters.poll(); + waiter = waiters.poll(); } closureFuture = SettableFuture.create(); retFuture = closureFuture; pendingClosure = - totalSessions() - + numSessionsBeingCreated - + 2 /* For pool maintenance thread + prepareExecutor */; + totalSessions() + numSessionsBeingCreated + 1 /* For pool maintenance thread */; poolMaintainer.close(); - readSessions.clear(); - writePreparedSessions.clear(); - prepareExecutor.shutdown(); - executor.submit( - new Runnable() { - @Override - public void run() { - try { - prepareExecutor.awaitTermination(5L, TimeUnit.SECONDS); - } catch (Throwable t) { - } - synchronized (lock) { - decrementPendingClosures(1); - } - } - }); + sessions.clear(); for (PooledSessionFuture session : checkedOutSessions) { if (session.leakedException != null) { if (options.isFailOnSessionLeak()) { @@ -2468,29 +2091,9 @@ public void run() { return retFuture; } - private boolean shouldUnblockReader() { - // This might not be the best strategy since a continuous burst of read requests can starve - // a write request. Maybe maintain a timestamp in the queue and unblock according to that - // or just flip a weighted coin. - synchronized (lock) { - int numWriteWaiters = readWriteWaiters.size() - numSessionsBeingPrepared; - return readWaiters.size() > numWriteWaiters; - } - } - - private boolean shouldPrepareSession() { - synchronized (lock) { - if (stopAutomaticPrepare) { - return false; - } - int preparedSessions = writePreparedSessions.size() + numSessionsBeingPrepared; - return preparedSessions < Math.floor(options.getWriteSessionsFraction() * totalSessions()); - } - } - private int numWaiters() { synchronized (lock) { - return readWaiters.size() + readWriteWaiters.size(); + return waiters.size(); } } @@ -2525,43 +2128,6 @@ public void run() { return res; } - private void prepareSession(final PooledSession sess) { - synchronized (lock) { - numSessionsBeingPrepared++; - } - prepareExecutor.submit( - new Runnable() { - @Override - public void run() { - try { - logger.log(Level.FINE, "Preparing session"); - sess.prepareReadWriteTransaction(); - logger.log(Level.FINE, "Session prepared"); - synchronized (lock) { - numSessionsAsyncPrepared++; - numSessionsBeingPrepared--; - if (!isClosed()) { - if (readWriteWaiters.size() > 0) { - readWriteWaiters.poll().put(sess); - } else if (readWaiters.size() > 0) { - readWaiters.poll().put(sess); - } else { - writePreparedSessions.add(sess); - } - } - } - } catch (Throwable t) { - synchronized (lock) { - numSessionsBeingPrepared--; - if (!isClosed()) { - handlePrepareSessionFailure(newSpannerException(t), sess, true); - } - } - } - } - }); - } - /** * Returns the minimum of the wanted number of sessions that the caller wants to create and the * actual max number that may be created at this moment. @@ -2770,7 +2336,8 @@ public long applyAsLong(SessionPool sessionPool) { new ToLongFunction() { @Override public long applyAsLong(SessionPool sessionPool) { - return sessionPool.numSessionsBeingPrepared; + // TODO: Remove metric. + return 0L; } }); @@ -2794,7 +2361,7 @@ public long applyAsLong(SessionPool sessionPool) { new ToLongFunction() { @Override public long applyAsLong(SessionPool sessionPool) { - return sessionPool.readSessions.size(); + return sessionPool.sessions.size(); } }); @@ -2806,7 +2373,8 @@ public long applyAsLong(SessionPool sessionPool) { new ToLongFunction() { @Override public long applyAsLong(SessionPool sessionPool) { - return sessionPool.writePreparedSessions.size(); + // TODO: Remove metric. + return 0L; } }); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java index 57dbd4debd4..2c68fd317e8 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java @@ -31,7 +31,12 @@ public class SessionPoolOptions { private final int maxSessions; private final int incStep; private final int maxIdleSessions; - private final float writeSessionsFraction; + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This setting therefore does not have any meaning anymore, and may be removed in the future. + */ + @Deprecated private final float writeSessionsFraction; + private final ActionOnExhaustion actionOnExhaustion; private final long loopFrequency; private final int keepAliveIntervalMinutes; @@ -74,6 +79,13 @@ public int getMaxIdleSessions() { return maxIdleSessions; } + /** + * @deprecated This value is no longer used. The session pool does not prepare any sessions for + * read/write transactions. Instead, a transaction will be started by including a + * BeginTransaction option with the first statement of a transaction. This method may be + * removed in a future release. + */ + @Deprecated public float getWriteSessionsFraction() { return writeSessionsFraction; } @@ -139,7 +151,12 @@ public static class Builder { private int maxSessions = DEFAULT_MAX_SESSIONS; private int incStep = DEFAULT_INC_STEP; private int maxIdleSessions; - private float writeSessionsFraction = 0.2f; + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This setting therefore does not have any meaning anymore, and may be removed in the future. + */ + @Deprecated private float writeSessionsFraction = 0.2f; + private ActionOnExhaustion actionOnExhaustion = DEFAULT_ACTION; private long initialWaitForSessionTimeoutMillis = 30_000L; private ActionOnSessionNotFound actionOnSessionNotFound = ActionOnSessionNotFound.RETRY; @@ -260,12 +277,11 @@ Builder setFailOnSessionLeak() { } /** - * Fraction of sessions to be kept prepared for write transactions. This is an optimisation to - * avoid the cost of sending a BeginTransaction() rpc. If all such sessions are in use and a - * write request comes, we will make the BeginTransaction() rpc inline. It must be between 0 and - * 1(inclusive). - * - *

    Default value is 0.2. + * @deprecated This configuration value is no longer in use. The session pool does not prepare + * any sessions for read/write transactions. Instead, a transaction will automatically be + * started by the first statement that is executed by a transaction by including a + * BeginTransaction option with that statement. + *

    This method may be removed in a future release. */ public Builder setWriteSessionsFraction(float writeSessionsFraction) { this.writeSessionsFraction = writeSessionsFraction; @@ -288,9 +304,6 @@ private void validate() { } Preconditions.checkArgument( keepAliveIntervalMinutes < 60, "Keep alive interval should be less than" + "60 minutes"); - Preconditions.checkArgument( - writeSessionsFraction >= 0 && writeSessionsFraction <= 1, - "Fraction of write sessions must be between 0 and 1 (inclusive)"); } } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index 8dbab883140..b18e2f25d9a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -54,7 +54,6 @@ public TransactionContext begin() { try (Scope s = tracer.withSpan(span)) { txn = session.newTransaction(); session.setActive(this); - txn.ensureTxn(); txnState = TransactionState.STARTED; return txn; } @@ -101,8 +100,11 @@ public TransactionContext resetForRetry() { "resetForRetry can only be called if the previous attempt" + " aborted"); } try (Scope s = tracer.withSpan(span)) { + boolean useInlinedBegin = txn.transactionId != null; txn = session.newTransaction(); - txn.ensureTxn(); + if (!useInlinedBegin) { + txn.ensureTxn(); + } txnState = TransactionState.STARTED; return txn; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index ab4a80b340f..e38b704f700 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import com.google.api.core.ApiAsyncFunction; import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; @@ -45,6 +44,8 @@ import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; import com.google.spanner.v1.ResultSet; import com.google.spanner.v1.RollbackRequest; +import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.TransactionOptions; import com.google.spanner.v1.TransactionSelector; import io.opencensus.common.Scope; import io.opencensus.trace.AttributeValue; @@ -150,7 +151,15 @@ public void removeListener(Runnable listener) { @GuardedBy("lock") private long retryDelayInMillis = -1L; - private volatile ByteString transactionId; + /** + * transactionIdFuture will return the transaction id returned by the first statement in the + * transaction if the BeginTransaction option is included with the first statement of the + * transaction. + */ + private volatile SettableApiFuture transactionIdFuture = null; + + volatile ByteString transactionId; + private Timestamp commitTimestamp; private TransactionContextImpl(Builder builder) { @@ -190,36 +199,7 @@ void ensureTxn() { ApiFuture ensureTxnAsync() { final SettableApiFuture res = SettableApiFuture.create(); if (transactionId == null || isAborted()) { - span.addAnnotation("Creating Transaction"); - final ApiFuture fut = session.beginTransactionAsync(); - fut.addListener( - new Runnable() { - @Override - public void run() { - try { - transactionId = fut.get(); - span.addAnnotation( - "Transaction Creation Done", - ImmutableMap.of( - "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8()))); - txnLogger.log( - Level.FINER, - "Started transaction {0}", - txnLogger.isLoggable(Level.FINER) - ? transactionId.asReadOnlyByteBuffer() - : null); - res.set(null); - } catch (ExecutionException e) { - span.addAnnotation( - "Transaction Creation Failed", - TraceUtil.getExceptionAnnotations(e.getCause() == null ? e : e.getCause())); - res.setException(e.getCause() == null ? e : e.getCause()); - } catch (InterruptedException e) { - res.setException(SpannerExceptionFactory.propagateInterrupt(e)); - } - } - }, - MoreExecutors.directExecutor()); + createTxnAsync(res); } else { span.addAnnotation( "Transaction Initialized", @@ -234,6 +214,39 @@ public void run() { return res; } + private void createTxnAsync(final SettableApiFuture res) { + span.addAnnotation("Creating Transaction"); + final ApiFuture fut = session.beginTransactionAsync(); + fut.addListener( + new Runnable() { + @Override + public void run() { + try { + transactionId = fut.get(); + span.addAnnotation( + "Transaction Creation Done", + ImmutableMap.of( + "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8()))); + txnLogger.log( + Level.FINER, + "Started transaction {0}", + txnLogger.isLoggable(Level.FINER) + ? transactionId.asReadOnlyByteBuffer() + : null); + res.set(null); + } catch (ExecutionException e) { + span.addAnnotation( + "Transaction Creation Failed", + TraceUtil.getExceptionAnnotations(e.getCause() == null ? e : e.getCause())); + res.setException(e.getCause() == null ? e : e.getCause()); + } catch (InterruptedException e) { + res.setException(SpannerExceptionFactory.propagateInterrupt(e)); + } + } + }, + MoreExecutors.directExecutor()); + } + void commit() { try { commitTimestamp = commitAsync().get(); @@ -251,86 +264,103 @@ void commit() { ApiFuture commitAsync() { final SettableApiFuture res = SettableApiFuture.create(); - final SettableApiFuture latch; + final SettableApiFuture finishOps; + CommitRequest.Builder builder = CommitRequest.newBuilder().setSession(session.getName()); synchronized (lock) { - latch = finishedAsyncOperations; + if (transactionIdFuture == null && transactionId == null) { + finishOps = SettableApiFuture.create(); + createTxnAsync(finishOps); + } else { + finishOps = finishedAsyncOperations; + } + if (!mutations.isEmpty()) { + List mutationsProto = new ArrayList<>(); + Mutation.toProto(mutations, mutationsProto); + builder.addAllMutations(mutationsProto); + } + // Ensure that no call to buffer mutations that would be lost can succeed. + mutations = null; } - latch.addListener( - new Runnable() { - @Override - public void run() { - try { - latch.get(); - CommitRequest.Builder builder = - CommitRequest.newBuilder() - .setSession(session.getName()) - .setTransactionId(transactionId); - synchronized (lock) { - if (!mutations.isEmpty()) { - List mutationsProto = new ArrayList<>(); - Mutation.toProto(mutations, mutationsProto); - builder.addAllMutations(mutationsProto); - } - // Ensure that no call to buffer mutations that would be lost can succeed. - mutations = null; - } - final CommitRequest commitRequest = builder.build(); - span.addAnnotation("Starting Commit"); - final Span opSpan = - tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); - commitFuture = rpc.commitAsync(commitRequest, session.getOptions()); - commitFuture.addListener( - tracer.withSpan( - opSpan, - new Runnable() { - @Override - public void run() { - try { - CommitResponse commitResponse = commitFuture.get(); - if (!commitResponse.hasCommitTimestamp()) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Missing commitTimestamp:\n" + session.getName()); - } - Timestamp ts = - Timestamp.fromProto(commitResponse.getCommitTimestamp()); - span.addAnnotation("Commit Done"); - opSpan.end(TraceUtil.END_SPAN_OPTIONS); - res.set(ts); - } catch (Throwable e) { - if (e instanceof ExecutionException) { - e = - SpannerExceptionFactory.newSpannerException( - e.getCause() == null ? e : e.getCause()); - } else if (e instanceof InterruptedException) { - e = - SpannerExceptionFactory.propagateInterrupt( - (InterruptedException) e); - } else { - e = SpannerExceptionFactory.newSpannerException(e); - } - span.addAnnotation( - "Commit Failed", TraceUtil.getExceptionAnnotations(e)); - TraceUtil.endSpanWithFailure(opSpan, e); - onError((SpannerException) e); - res.setException(e); - } - } - }), - MoreExecutors.directExecutor()); - } catch (InterruptedException e) { - res.setException(SpannerExceptionFactory.propagateInterrupt(e)); - } catch (ExecutionException e) { - res.setException( - SpannerExceptionFactory.newSpannerException( - e.getCause() == null ? e : e.getCause())); - } - } - }, - MoreExecutors.directExecutor()); + finishOps.addListener( + new CommitRunnable(res, finishOps, builder), MoreExecutors.directExecutor()); return res; } + private final class CommitRunnable implements Runnable { + private final SettableApiFuture res; + private final ApiFuture prev; + private final CommitRequest.Builder requestBuilder; + + CommitRunnable( + SettableApiFuture res, + ApiFuture prev, + CommitRequest.Builder requestBuilder) { + this.res = res; + this.prev = prev; + this.requestBuilder = requestBuilder; + } + + @Override + public void run() { + try { + prev.get(); + if (transactionId == null && transactionIdFuture == null) { + requestBuilder.setSingleUseTransaction( + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())); + } else { + requestBuilder.setTransactionId( + transactionId == null ? transactionIdFuture.get() : transactionId); + } + final CommitRequest commitRequest = requestBuilder.build(); + span.addAnnotation("Starting Commit"); + final Span opSpan = + tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); + final ApiFuture commitFuture = + rpc.commitAsync(commitRequest, session.getOptions()); + commitFuture.addListener( + tracer.withSpan( + opSpan, + new Runnable() { + @Override + public void run() { + try { + CommitResponse commitResponse = commitFuture.get(); + if (!commitResponse.hasCommitTimestamp()) { + throw newSpannerException( + ErrorCode.INTERNAL, "Missing commitTimestamp:\n" + session.getName()); + } + Timestamp ts = Timestamp.fromProto(commitResponse.getCommitTimestamp()); + span.addAnnotation("Commit Done"); + opSpan.end(TraceUtil.END_SPAN_OPTIONS); + res.set(ts); + } catch (Throwable e) { + if (e instanceof ExecutionException) { + e = + SpannerExceptionFactory.newSpannerException( + e.getCause() == null ? e : e.getCause()); + } else if (e instanceof InterruptedException) { + e = SpannerExceptionFactory.propagateInterrupt((InterruptedException) e); + } else { + e = SpannerExceptionFactory.newSpannerException(e); + } + span.addAnnotation("Commit Failed", TraceUtil.getExceptionAnnotations(e)); + TraceUtil.endSpanWithFailure(opSpan, e); + onError((SpannerException) e, false); + res.setException(e); + } + } + }), + MoreExecutors.directExecutor()); + } catch (InterruptedException e) { + res.setException(SpannerExceptionFactory.propagateInterrupt(e)); + } catch (ExecutionException e) { + res.setException( + SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause())); + } + } + } + Timestamp commitTimestamp() { checkState(commitTimestamp != null, "run() has not yet returned normally"); return commitTimestamp; @@ -343,54 +373,110 @@ boolean isAborted() { } void rollback() { - // We're exiting early due to a user exception, but the transaction is still active. - // Send a rollback for the transaction to release any locks held. - // TODO(user): Make this an async fire-and-forget request. try { - // Note that we're not retrying this request since we don't particularly care about the - // response. Normally, the next thing that will happen is that we will make a fresh - // transaction attempt, which should implicitly abort this one. + rollbackAsync().get(); + } catch (ExecutionException e) { + txnLogger.log(Level.FINE, "Exception during rollback", e); + span.addAnnotation("Rollback Failed", TraceUtil.getExceptionAnnotations(e)); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + + ApiFuture rollbackAsync() { + // It could be that there is no transaction if the transaction has been marked + // withInlineBegin, and there has not been any query/update statement that has been executed. + // In that case, we do not need to do anything, as there is no transaction. + // + // We do not take the transactionLock before trying to rollback to prevent a rollback call + // from blocking if an async query or update statement that is trying to begin the transaction + // is still in flight. That transaction will then automatically be terminated by the server. + if (transactionId != null) { span.addAnnotation("Starting Rollback"); - rpc.rollback( + return rpc.rollbackAsync( RollbackRequest.newBuilder() .setSession(session.getName()) .setTransactionId(transactionId) .build(), session.getOptions()); - span.addAnnotation("Rollback Done"); - } catch (SpannerException e) { - txnLogger.log(Level.FINE, "Exception during rollback", e); - span.addAnnotation("Rollback Failed", TraceUtil.getExceptionAnnotations(e)); + } else { + return ApiFutures.immediateFuture(Empty.getDefaultInstance()); } } - ApiFuture rollbackAsync() { - span.addAnnotation("Starting Rollback"); - return ApiFutures.transformAsync( - rpc.rollbackAsync( - RollbackRequest.newBuilder() - .setSession(session.getName()) - .setTransactionId(transactionId) - .build(), - session.getOptions()), - new ApiAsyncFunction() { - @Override - public ApiFuture apply(Empty input) throws Exception { - span.addAnnotation("Rollback Done"); - return ApiFutures.immediateFuture(null); - } - }, - MoreExecutors.directExecutor()); - } - @Nullable @Override TransactionSelector getTransactionSelector() { + // Check if there is already a transactionId available. That is the case if this transaction + // has already been prepared by the session pool, or if this transaction has been marked + // withInlineBegin and an earlier statement has already started a transaction. + if (transactionId == null) { + try { + ApiFuture tx = null; + synchronized (lock) { + // The first statement of a transaction that gets here will be the one that includes + // BeginTransaction with the statement. The others will be waiting on the + // transactionIdFuture until an actual transactionId is available. + if (transactionIdFuture == null) { + transactionIdFuture = SettableApiFuture.create(); + } else { + tx = transactionIdFuture; + } + } + if (tx == null) { + return TransactionSelector.newBuilder() + .setBegin( + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) + .build(); + } else { + // Wait for the transaction to come available. The tx.get() call will fail with an + // Aborted error if the call that included the BeginTransaction option fails. The + // Aborted error will cause the entire transaction to be retried, and the retry will use + // a separate BeginTransaction RPC. + TransactionSelector.newBuilder().setId(tx.get()).build(); + } + } catch (ExecutionException e) { + if (e.getCause() instanceof AbortedException) { + synchronized (lock) { + aborted = true; + } + } + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.newSpannerExceptionForCancellation(null, e); + } + } + // There is already a transactionId available. Include that id as the transaction to use. return TransactionSelector.newBuilder().setId(transactionId).build(); } @Override - public void onError(SpannerException e) { + public void onTransactionMetadata(Transaction transaction) { + // A transaction has been returned by a statement that was executed. Set the id of the + // transaction on this instance and release the lock to allow other statements to proceed. + if (this.transactionId == null && transaction != null && transaction.getId() != null) { + this.transactionId = transaction.getId(); + this.transactionIdFuture.set(transaction.getId()); + } + } + + @Override + public void onError(SpannerException e, boolean withBeginTransaction) { + // If the statement that caused an error was the statement that included a BeginTransaction + // option, we simulate an aborted transaction to force a retry of the entire transaction. This + // will cause the retry to execute an explicit BeginTransaction RPC and then the actual + // statements of the transaction. This is needed as the first statement of the transaction + // must be included with the transaction to ensure that any locks that are taken by the + // statement are included in the transaction, even if the statement again causes an error + // during the retry. + if (withBeginTransaction) { + // Simulate an aborted transaction to force a retry with a new transaction. + this.transactionIdFuture.setException( + SpannerExceptionFactory.newSpannerException( + ErrorCode.ABORTED, "Aborted due to failed initial statement", e)); + } + if (e.getErrorCode() == ErrorCode.ABORTED) { long delay = -1L; if (e instanceof AbortedException) { @@ -433,6 +519,9 @@ public long executeUpdate(Statement statement) { try { com.google.spanner.v1.ResultSet resultSet = rpc.executeQuery(builder.build(), session.getOptions()); + if (resultSet.getMetadata().hasTransaction()) { + onTransactionMetadata(resultSet.getMetadata().getTransaction()); + } if (!resultSet.hasStats()) { throw new IllegalArgumentException( "DML response missing stats possibly due to non-DML statement as input"); @@ -440,7 +529,7 @@ public long executeUpdate(Statement statement) { // For standard DML, using the exact row count. return resultSet.getStats().getRowCountExact(); } catch (SpannerException e) { - onError(e); + onError(e, builder.hasTransaction() && builder.getTransaction().hasBegin()); throw e; } } @@ -450,7 +539,7 @@ public ApiFuture executeUpdateAsync(Statement statement) { beforeReadOrQuery(); final ExecuteSqlRequest.Builder builder = getExecuteSqlRequestBuilder(statement, QueryMode.NORMAL); - ApiFuture resultSet; + final ApiFuture resultSet; try { // Register the update as an async operation that must finish before the transaction may // commit. @@ -484,7 +573,7 @@ public Long apply(ResultSet input) { @Override public Long apply(Throwable input) { SpannerException e = SpannerExceptionFactory.newSpannerException(input); - onError(e); + onError(e, builder.hasTransaction() && builder.getTransaction().hasBegin()); throw e; } }, @@ -493,6 +582,14 @@ public Long apply(Throwable input) { new Runnable() { @Override public void run() { + try { + if (resultSet.get().getMetadata().hasTransaction()) { + onTransactionMetadata(resultSet.get().getMetadata().getTransaction()); + } + } catch (ExecutionException | InterruptedException e) { + // Ignore this error here as it is handled by the future that is returned by the + // executeUpdateAsync method. + } decreaseAsyncOperations(); } }, @@ -510,6 +607,9 @@ public long[] batchUpdate(Iterable statements) { long[] results = new long[response.getResultSetsCount()]; for (int i = 0; i < response.getResultSetsCount(); ++i) { results[i] = response.getResultSets(i).getStats().getRowCountExact(); + if (response.getResultSets(i).getMetadata().hasTransaction()) { + onTransactionMetadata(response.getResultSets(i).getMetadata().getTransaction()); + } } // If one of the DML statements was aborted, we should throw an aborted exception. @@ -525,7 +625,7 @@ public long[] batchUpdate(Iterable statements) { } return results; } catch (SpannerException e) { - onError(e); + onError(e, builder.hasTransaction() && builder.getTransaction().hasBegin()); throw e; } } @@ -553,6 +653,9 @@ public long[] apply(ExecuteBatchDmlResponse input) { long[] results = new long[input.getResultSetsCount()]; for (int i = 0; i < input.getResultSetsCount(); ++i) { results[i] = input.getResultSets(i).getStats().getRowCountExact(); + if (input.getResultSets(i).getMetadata().hasTransaction()) { + onTransactionMetadata(input.getResultSets(i).getMetadata().getTransaction()); + } } // If one of the DML statements was aborted, we should throw an aborted exception. // In all other cases, we should throw a BatchUpdateException. @@ -577,7 +680,9 @@ public long[] apply(ExecuteBatchDmlResponse input) { @Override public long[] apply(Throwable input) { SpannerException e = SpannerExceptionFactory.newSpannerException(input); - onError(e); + onError( + SpannerExceptionFactory.newSpannerException(e.getCause()), + builder.hasTransaction() && builder.getTransaction().hasBegin()); throw e; } }, @@ -663,19 +768,25 @@ private T runInternal(final TransactionCallable txCallable) { new Callable() { @Override public T call() { + boolean useInlinedBegin = true; if (attempt.get() > 0) { + // Do not inline the BeginTransaction during a retry if the initial attempt did not + // actually start a transaction. + useInlinedBegin = txn.transactionId != null; txn = session.newTransaction(); } checkState( isValid, "TransactionRunner has been invalidated by a new operation on the session"); attempt.incrementAndGet(); - // TODO(user): When using streaming reads, consider using the first read to begin - // the txn. span.addAnnotation( "Starting Transaction Attempt", ImmutableMap.of("Attempt", AttributeValue.longAttributeValue(attempt.longValue()))); - txn.ensureTxn(); + // Only ensure that there is a transaction if we should not inline the beginTransaction + // with the first statement. + if (!useInlinedBegin) { + txn.ensureTxn(); + } T result; boolean shouldRollback = true; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java index 3869dbdfcfe..2af185ae144 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java @@ -140,7 +140,7 @@ public void asyncRunnerUpdateAborted() throws Exception { @Override public ApiFuture doWorkAsync(TransactionContext txn) { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } else { // Set the result of the update statement back to 1 row. mockSpanner.putStatementResult( @@ -199,7 +199,7 @@ public void asyncRunnerUpdateAbortedWithoutGettingResult() throws Exception { @Override public ApiFuture doWorkAsync(TransactionContext txn) { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } // This update statement will be aborted, but the error will not propagated to the // transaction runner and cause the transaction to retry. Instead, the commit call @@ -217,9 +217,9 @@ public ApiFuture doWorkAsync(TransactionContext txn) { assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteSqlRequest.class, - CommitRequest.class, + // The retry will use an explicit BeginTransaction RPC because the first statement of + // the transaction did not return a transaction id during the initial attempt. BeginTransactionRequest.class, ExecuteSqlRequest.class, CommitRequest.class); @@ -272,10 +272,7 @@ public ApiFuture doWorkAsync(TransactionContext txn) { res.get(); assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteSqlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class); } @Test @@ -418,9 +415,14 @@ public void asyncRunnerBatchUpdateAbortedWithoutGettingResult() throws Exception @Override public ApiFuture doWorkAsync(TransactionContext txn) { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextTransaction(); } - // This update statement will be aborted, but the error will not propagated to the + // This statement will succeed and return a transaction id. The transaction will be + // marked as aborted on the mock server. + txn.executeUpdate(UPDATE_STATEMENT); + + // This batch update statement will be aborted, but the error will not propagated to + // the // transaction runner and cause the transaction to retry. Instead, the commit call // will do that. txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); @@ -436,10 +438,10 @@ public ApiFuture doWorkAsync(TransactionContext txn) { assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, + ExecuteSqlRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class, - BeginTransactionRequest.class, + ExecuteSqlRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @@ -491,10 +493,7 @@ public ApiFuture doWorkAsync(TransactionContext txn) { res.get(); assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteBatchDmlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java index 1d1b16d0148..ddf8f580a50 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java @@ -330,7 +330,7 @@ public ApiFuture apply(TransactionContext txn, Long input) public void asyncTransactionManagerFireAndForgetInvalidUpdate() throws Exception { final SettableApiFuture updateCount = SettableApiFuture.create(); - try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { TransactionContextFuture txn = mgr.beginAsync(); while (true) { try { @@ -341,6 +341,8 @@ public void asyncTransactionManagerFireAndForgetInvalidUpdate() throws Exception public ApiFuture apply(TransactionContext txn, Void input) throws Exception { // This fire-and-forget update statement should not fail the transaction. + // The exception will however cause the transaction to be retried, as the + // statement will not return a transaction id. txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); ApiFutures.addCallback( txn.executeUpdateAsync(UPDATE_STATEMENT), @@ -361,14 +363,26 @@ public void onSuccess(Long result) { }, executor) .commitAsync(); - assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); assertThat(ts.get()).isNotNull(); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); break; } catch (AbortedException e) { txn = mgr.resetForRetryAsync(); } } } + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + // The first update that fails. This will cause a transaction retry. + ExecuteSqlRequest.class, + // The retry will use an explicit BeginTransaction call. + BeginTransactionRequest.class, + // The first update will again fail, but now there is a transaction id, so the + // transaction can continue. + ExecuteSqlRequest.class, + ExecuteSqlRequest.class, + CommitRequest.class); } @Test @@ -468,7 +482,7 @@ public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { // Abort the first attempt. - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } else { // Set the result of the update statement back to 1 row. mockSpanner.putStatementResult( @@ -508,7 +522,7 @@ public void asyncTransactionManagerUpdateAbortedWithoutGettingResult() throws Ex public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } // This update statement will be aborted, but the error will not // propagated to the transaction runner and cause the transaction to @@ -530,8 +544,8 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsAtLeast( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteSqlRequest.class, + // The retry will use a BeginTransaction RPC. BeginTransactionRequest.class, ExecuteSqlRequest.class, CommitRequest.class); @@ -595,10 +609,7 @@ public ApiFuture apply(TransactionContext txn, Void input) .get(); assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteSqlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class); break; } catch (AbortedException e) { txn = mgr.resetForRetryAsync(); @@ -714,7 +725,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); @@ -756,7 +766,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, @@ -776,7 +785,7 @@ public void asyncTransactionManagerBatchUpdateAbortedBeforeFirstStatement() thro public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } return txn.batchUpdateAsync( ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); @@ -797,7 +806,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, @@ -859,7 +867,6 @@ public ApiFuture apply(TransactionContext txn, long[] input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class, BeginTransactionRequest.class, @@ -880,7 +887,7 @@ public void asyncTransactionManagerBatchUpdateAbortedWithoutGettingResult() thro public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } // This update statement will be aborted, but the error will not propagated to // the transaction manager and cause the transaction to retry. Instead, the @@ -904,12 +911,11 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(attempt.get()).isEqualTo(2); Iterable> requests = mockSpanner.getRequestTypes(); int size = Iterables.size(requests); - assertThat(size).isIn(Range.closed(6, 7)); - if (size == 6) { + assertThat(size).isIn(Range.closed(5, 6)); + if (size == 5) { assertThat(requests) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, @@ -918,7 +924,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(requests) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class, BeginTransactionRequest.class, @@ -958,10 +963,7 @@ public void asyncTransactionManagerWithBatchUpdateCommitFails() throws Exception } assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteBatchDmlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @Test @@ -990,10 +992,7 @@ public ApiFuture apply(TransactionContext txn, Void input) } assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteBatchDmlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java index abac3bd1348..7dac8c8bfed 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java @@ -19,13 +19,11 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; -import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.common.base.Stopwatch; import com.google.protobuf.ListValue; import com.google.spanner.v1.ResultSetMetadata; @@ -235,72 +233,4 @@ public void testSpannerReturnsResourceExhausted() throws InterruptedException { // Verify that all sessions have been deleted. assertThat(client.pool.totalSessions(), is(equalTo(0))); } - - @Test - public void testPrepareSessionFailPropagatesToUser() { - // Do not create any sessions by default. - // This also means that when a read/write session is requested, the session pool - // will start preparing a read session at that time. Any errors that might occur - // during the BeginTransaction call will be propagated to the user. - int minSessions = 0; - int maxSessions = 1000; - DatabaseClientImpl client = null; - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofStickyException( - Status.ABORTED.withDescription("BeginTransaction failed").asRuntimeException())); - try (Spanner spanner = createSpanner(minSessions, maxSessions)) { - client = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - TransactionRunner runner = client.readWriteTransaction(); - runner.run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode(), is(equalTo(ErrorCode.ABORTED))); - assertThat(e.getMessage().endsWith("BeginTransaction failed"), is(true)); - } - } - - @Test - public void testPrepareSessionFailDoesNotPropagateToUser() throws InterruptedException { - // Create 5 sessions and 20% write prepared sessions. - // That should prepare exactly 1 session for r/w. - int minSessions = 5; - int maxSessions = 1000; - DatabaseClientImpl client = null; - // The first prepare should fail. - // The prepare will then be retried and should succeed. - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofException( - Status.ABORTED.withDescription("BeginTransaction failed").asRuntimeException())); - try (Spanner spanner = createSpanner(minSessions, maxSessions)) { - client = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - // Wait until the session pool has initialized and a session has been prepared. - Stopwatch watch = Stopwatch.createStarted(); - while ((client.pool.totalSessions() < minSessions - || client.pool.getNumberOfAvailableWritePreparedSessions() != 1) - && watch.elapsed(TimeUnit.SECONDS) < 10) { - Thread.sleep(10L); - } - - // There should be 1 prepared session and a r/w transaction should succeed. - assertThat(client.pool.getNumberOfAvailableWritePreparedSessions(), is(equalTo(1))); - TransactionRunner runner = client.readWriteTransaction(); - runner.run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - } - } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index 8775fb1b183..2747dc314f7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -809,65 +809,6 @@ public void testPartitionedDmlRetriesOnUnavailable() { } } - @Test - public void testDatabaseOrInstanceDoesNotExistOnPrepareSession() throws Exception { - StatusRuntimeException[] exceptions = - new StatusRuntimeException[] { - SpannerExceptionFactoryTest.newStatusResourceNotFoundException( - "Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, DATABASE_NAME), - SpannerExceptionFactoryTest.newStatusResourceNotFoundException( - "Instance", SpannerExceptionFactory.INSTANCE_RESOURCE_TYPE, INSTANCE_NAME) - }; - for (StatusRuntimeException exception : exceptions) { - try (Spanner spanner = - SpannerOptions.newBuilder() - .setProjectId(TEST_PROJECT) - .setChannelProvider(channelProvider) - .setCredentials(NoCredentials.getInstance()) - .build() - .getService()) { - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofStickyException(exception)); - DatabaseClientImpl dbClient = - (DatabaseClientImpl) - spanner.getDatabaseClient( - DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - // Wait until all sessions have been created. - Stopwatch watch = Stopwatch.createStarted(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingCreated() > 0) { - Thread.sleep(1L); - } - // Ensure that no sessions could be prepared and that the session pool gives up trying to - // prepare sessions. - watch = watch.reset().start(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingPrepared() > 0) { - Thread.sleep(1L); - } - assertThat(dbClient.pool.getNumberOfSessionsBeingPrepared()).isEqualTo(0); - assertThat(dbClient.pool.getNumberOfAvailableWritePreparedSessions()).isEqualTo(0); - int currentNumRequest = mockSpanner.getRequests().size(); - try { - dbClient - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - fail("missing expected exception"); - } catch (DatabaseNotFoundException | InstanceNotFoundException e) { - } - assertThat(mockSpanner.getRequests()).hasSize(currentNumRequest); - mockSpanner.reset(); - mockSpanner.removeAllExecutionTimes(); - } - } - } - @Test public void testDatabaseOrInstanceDoesNotExistOnInitialization() throws Exception { StatusRuntimeException[] exceptions = @@ -1001,89 +942,6 @@ public void testDatabaseOrInstanceDoesNotExistOnReplenish() throws Exception { } } - @Test - public void testPermissionDeniedOnPrepareSession() throws Exception { - testExceptionOnPrepareSession( - Status.PERMISSION_DENIED - .withDescription( - "Caller is missing IAM permission spanner.databases.beginOrRollbackReadWriteTransaction on resource") - .asRuntimeException()); - } - - @Test - public void testFailedPreconditionOnPrepareSession() throws Exception { - testExceptionOnPrepareSession( - Status.FAILED_PRECONDITION - .withDescription("FAILED_PRECONDITION: Database is in read-only mode") - .asRuntimeException()); - } - - private void testExceptionOnPrepareSession(StatusRuntimeException exception) - throws InterruptedException { - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofStickyException(exception)); - DatabaseClientImpl dbClient = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - // Wait until all sessions have been created. - Stopwatch watch = Stopwatch.createStarted(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingCreated() > 0) { - Thread.sleep(1L); - } - // Ensure that no sessions could be prepared and that the session pool gives up trying to - // prepare sessions. - watch = watch.reset().start(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingPrepared() > 0) { - Thread.sleep(1L); - } - assertThat(dbClient.pool.getNumberOfSessionsBeingPrepared()).isEqualTo(0); - assertThat(dbClient.pool.getNumberOfAvailableWritePreparedSessions()).isEqualTo(0); - try { - dbClient - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - fail(String.format("missing expected %s exception", exception.getStatus().getCode().name())); - } catch (SpannerException e) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.fromGrpcStatus(exception.getStatus())); - } - // Remove the semi-permanent error condition. Getting a read/write transaction should now - // succeed, and the automatic preparing of sessions should be restarted. - mockSpanner.setBeginTransactionExecutionTime(SimulatedExecutionTime.none()); - dbClient - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - for (int i = 0; i < spanner.getOptions().getSessionPoolOptions().getMinSessions(); i++) { - dbClient.pool.getReadSession().close(); - } - int expectedPreparedSessions = - (int) - Math.ceil( - dbClient.pool.getNumberOfSessionsInPool() - * spanner.getOptions().getSessionPoolOptions().getWriteSessionsFraction()); - watch = watch.reset().start(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfAvailableWritePreparedSessions() < expectedPreparedSessions) { - Thread.sleep(1L); - } - assertThat(dbClient.pool.getNumberOfSessionsBeingPrepared()).isEqualTo(0); - assertThat(dbClient.pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo(expectedPreparedSessions); - } - /** * Test showing that when a database is deleted while it is in use by a database client and then * re-created with the same name, will continue to return {@link DatabaseNotFoundException}s until @@ -1113,8 +971,7 @@ public void testDatabaseOrInstanceIsDeletedAndThenRecreated() throws Exception { // Wait until all sessions have been created and prepared. Stopwatch watch = Stopwatch.createStarted(); while (watch.elapsed(TimeUnit.SECONDS) < 5 - && (dbClient.pool.getNumberOfSessionsBeingCreated() > 0 - || dbClient.pool.getNumberOfSessionsBeingPrepared() > 0)) { + && (dbClient.pool.getNumberOfSessionsBeingCreated() > 0)) { Thread.sleep(1L); } // Simulate that the database or instance has been deleted. diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index de1cb74c822..a2ab8dbc90e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -59,7 +59,7 @@ private static class NoOpListener implements AbstractResultSet.Listener { public void onTransactionMetadata(Transaction transaction) throws SpannerException {} @Override - public void onError(SpannerException e) {} + public void onError(SpannerException e, boolean withBeginTransaction) {} @Override public void onDone() {} @@ -77,11 +77,11 @@ public void cancel(@Nullable String message) {} public void request(int numMessages) {} }); consumer = stream.consumer(); - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); } public AbstractResultSet.GrpcResultSet resultSetWithMode(QueryMode queryMode) { - return new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + return new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); } @Test @@ -642,7 +642,7 @@ public com.google.protobuf.Value apply(@Nullable Value input) { private void verifySerialization( Function protoFn, Value... values) { - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); PartialResultSet.Builder builder = PartialResultSet.newBuilder(); List types = new ArrayList<>(); for (Value value : values) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java index 66256489e88..548f88172fd 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java @@ -98,12 +98,12 @@ public ScheduledExecutorService get() { @Test public void sessionCreation() { - try (PooledSessionFuture session = pool.getReadSession()) { + try (PooledSessionFuture session = pool.getSession()) { assertThat(session.get()).isNotNull(); } - try (PooledSessionFuture session = pool.getReadSession(); - PooledSessionFuture session2 = pool.getReadSession()) { + try (PooledSessionFuture session = pool.getSession(); + PooledSessionFuture session2 = pool.getSession()) { assertThat(session.get()).isNotNull(); assertThat(session2.get()).isNotNull(); } @@ -111,14 +111,14 @@ public void sessionCreation() { @Test public void poolExhaustion() throws Exception { - Session session1 = pool.getReadSession().get(); - Session session2 = pool.getReadSession().get(); + Session session1 = pool.getSession().get(); + Session session2 = pool.getSession().get(); final CountDownLatch latch = new CountDownLatch(1); new Thread( new Runnable() { @Override public void run() { - try (Session session3 = pool.getReadSession().get()) { + try (Session session3 = pool.getSession().get()) { latch.countDown(); } } @@ -132,8 +132,8 @@ public void run() { @Test public void multipleWaiters() throws Exception { - Session session1 = pool.getReadSession().get(); - Session session2 = pool.getReadSession().get(); + Session session1 = pool.getSession().get(); + Session session2 = pool.getSession().get(); int numSessions = 5; final CountDownLatch latch = new CountDownLatch(numSessions); for (int i = 0; i < numSessions; i++) { @@ -141,7 +141,7 @@ public void multipleWaiters() throws Exception { new Runnable() { @Override public void run() { - try (Session session = pool.getReadSession().get()) { + try (Session session = pool.getSession().get()) { latch.countDown(); } } @@ -161,13 +161,13 @@ public void closeQuicklyDoesNotBlockIndefinitely() throws Exception { @Test public void closeAfterInitialCreateDoesNotBlockIndefinitely() throws Exception { - pool.getReadSession().close(); + pool.getSession().close(); pool.closeAsync(new SpannerImpl.ClosedException()).get(); } @Test public void closeWhenSessionsActiveFinishes() throws Exception { - pool.getReadSession().get(); + pool.getSession().get(); // This will log a warning that a session has been leaked, as the session that we retrieved in // the previous statement was never returned to the pool. pool.closeAsync(new SpannerImpl.ClosedException()).get(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java new file mode 100644 index 00000000000..ecd8f4410d6 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java @@ -0,0 +1,264 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Benchmarks for inlining the BeginTransaction RPC with the first statement of a transaction. The + * simulated execution times are based on reasonable estimates and are primarily intended to keep + * the benchmarks comparable with each other before and after changes have been made to the pool. + * The benchmarks are bound to the Maven profile `benchmark` and can be executed like this: + * mvn clean test -DskipTests -Pbenchmark -Dbenchmark.name=InlineBeginBenchmark + * + */ +@BenchmarkMode(Mode.AverageTime) +@Fork(value = 1, warmups = 0) +@Measurement(batchSize = 1, iterations = 1, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(batchSize = 0, iterations = 0) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class InlineBeginBenchmark { + private static final String TEST_PROJECT = "my-project"; + private static final String TEST_INSTANCE = "my-instance"; + private static final String TEST_DATABASE = "my-database"; + private static final int HOLD_SESSION_TIME = 100; + private static final int RND_WAIT_TIME_BETWEEN_REQUESTS = 10; + private static final Random RND = new Random(); + + @State(Scope.Thread) + public static class BenchmarkState { + private final boolean useRealServer = Boolean.valueOf(System.getProperty("useRealServer")); + private final String instance = System.getProperty("instance", TEST_INSTANCE); + private final String database = System.getProperty("database", TEST_DATABASE); + private StandardBenchmarkMockServer mockServer; + private Spanner spanner; + private DatabaseClientImpl client; + + @Param({"true"}) + boolean inlineBegin; + + @Param({"0.2"}) + float writeFraction; + + @Setup(Level.Invocation) + public void setup() throws Exception { + System.out.println("useRealServer: " + System.getProperty("useRealServer")); + System.out.println("instance: " + System.getProperty("instance")); + SpannerOptions options; + if (useRealServer) { + System.out.println("running benchmark with **REAL** server"); + System.out.println("instance: " + instance); + System.out.println("database: " + database); + options = createRealServerOptions(); + } else { + System.out.println("running benchmark with **MOCK** server"); + mockServer = new StandardBenchmarkMockServer(); + TransportChannelProvider channelProvider = mockServer.start(); + options = createBenchmarkServerOptions(channelProvider); + } + + spanner = options.getService(); + client = + (DatabaseClientImpl) + spanner.getDatabaseClient(DatabaseId.of(options.getProjectId(), instance, database)); + Stopwatch watch = Stopwatch.createStarted(); + // Wait until the session pool has initialized. + while (client.pool.getNumberOfSessionsInPool() + < spanner.getOptions().getSessionPoolOptions().getMinSessions()) { + Thread.sleep(1L); + if (watch.elapsed(TimeUnit.SECONDS) > 10L) { + break; + } + } + } + + SpannerOptions createBenchmarkServerOptions(TransportChannelProvider channelProvider) { + return SpannerOptions.newBuilder() + .setProjectId(TEST_PROJECT) + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setWriteSessionsFraction(writeFraction).build()) + .build(); + } + + SpannerOptions createRealServerOptions() throws IOException { + return SpannerOptions.newBuilder() + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setWriteSessionsFraction(writeFraction).build()) + .build(); + } + + @TearDown(Level.Invocation) + public void teardown() throws Exception { + spanner.close(); + if (mockServer != null) { + mockServer.shutdown(); + } + } + } + + /** Measures the time needed to execute a burst of read requests. */ + @Benchmark + public void burstRead(final BenchmarkState server) throws Exception { + int totalQueries = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 8; + int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; + SessionPool pool = server.client.pool; + assertThat(pool.totalSessions()) + .isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + + ListeningScheduledExecutorService service = + MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); + List> futures = new ArrayList<>(totalQueries); + for (int i = 0; i < totalQueries; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + try (ResultSet rs = + server.client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { + while (rs.next()) { + Thread.sleep(RND.nextInt(HOLD_SESSION_TIME)); + } + return null; + } + } + })); + } + Futures.allAsList(futures).get(); + service.shutdown(); + } + + /** Measures the time needed to execute a burst of write requests. */ + @Benchmark + public void burstWrite(final BenchmarkState server) throws Exception { + int totalWrites = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 8; + int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; + SessionPool pool = server.client.pool; + assertThat(pool.totalSessions()) + .isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + + ListeningScheduledExecutorService service = + MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); + List> futures = new ArrayList<>(totalWrites); + for (int i = 0; i < totalWrites; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + TransactionRunner runner = server.client.readWriteTransaction(); + return runner.run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate( + StandardBenchmarkMockServer.UPDATE_STATEMENT); + } + }); + } + })); + } + Futures.allAsList(futures).get(); + service.shutdown(); + } + + /** Measures the time needed to execute a burst of read and write requests. */ + @Benchmark + public void burstReadAndWrite(final BenchmarkState server) throws Exception { + int totalWrites = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 4; + int totalReads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 4; + int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; + SessionPool pool = server.client.pool; + assertThat(pool.totalSessions()) + .isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + + ListeningScheduledExecutorService service = + MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); + List> futures = new ArrayList<>(totalReads + totalWrites); + for (int i = 0; i < totalWrites; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + TransactionRunner runner = server.client.readWriteTransaction(); + return runner.run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate( + StandardBenchmarkMockServer.UPDATE_STATEMENT); + } + }); + } + })); + } + for (int i = 0; i < totalReads; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + try (ResultSet rs = + server.client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { + while (rs.next()) { + Thread.sleep(RND.nextInt(HOLD_SESSION_TIME)); + } + return null; + } + } + })); + } + Futures.allAsList(futures).get(); + service.shutdown(); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java new file mode 100644 index 00000000000..d1e3d93cb71 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java @@ -0,0 +1,1155 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.grpc.testing.LocalChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionFunction; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; +import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ListValue; +import com.google.spanner.v1.BeginTransactionRequest; +import com.google.spanner.v1.CommitRequest; +import com.google.spanner.v1.ExecuteBatchDmlRequest; +import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.ReadRequest; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.RollbackRequest; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +import com.google.spanner.v1.TypeCode; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessServerBuilder; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InlineBeginTransactionTest { + @Parameter public Executor executor; + + @Parameters(name = "executor = {0}") + public static Collection data() { + return Arrays.asList( + new Object[][] { + {MoreExecutors.directExecutor()}, + {Executors.newSingleThreadExecutor()}, + {Executors.newFixedThreadPool(4)} + }); + } + + private static MockSpannerServiceImpl mockSpanner; + private static Server server; + private static LocalChannelProvider channelProvider; + private static final Statement UPDATE_STATEMENT = + Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2"); + private static final Statement INVALID_UPDATE_STATEMENT = + Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2"); + private static final long UPDATE_COUNT = 1L; + private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); + private static final ResultSetMetadata SELECT1_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("COL1") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.INT64) + .build()) + .build()) + .build()) + .build(); + private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build()) + .build()) + .setMetadata(SELECT1_METADATA) + .build(); + private static final Statement INVALID_SELECT = Statement.of("SELECT * FROM NON_EXISTING_TABLE"); + private static final Statement READ_STATEMENT = Statement.of("SELECT ID FROM FOO WHERE 1=1"); + + private Spanner spanner; + + @BeforeClass + public static void startStaticServer() throws IOException { + mockSpanner = new MockSpannerServiceImpl(); + mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET)); + mockSpanner.putStatementResult(StatementResult.query(READ_STATEMENT, SELECT1_RESULTSET)); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_UPDATE_STATEMENT, + Status.INVALID_ARGUMENT + .withDescription("invalid update statement") + .asRuntimeException())); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_SELECT, + Status.INVALID_ARGUMENT + .withDescription("invalid select statement") + .asRuntimeException())); + + String uniqueName = InProcessServerBuilder.generateName(); + server = + InProcessServerBuilder.forName(uniqueName) + // We need to use a real executor for timeouts to occur. + .scheduledExecutorService(new ScheduledThreadPoolExecutor(1)) + .addService(mockSpanner) + .build() + .start(); + channelProvider = LocalChannelProvider.create(uniqueName); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + } + + @Before + public void setUp() throws IOException { + mockSpanner.reset(); + mockSpanner.removeAllExecutionTimes(); + // Create a Spanner instance that will inline BeginTransaction calls. It also has no prepared + // sessions in the pool to prevent session preparing from interfering with test cases. + spanner = + SpannerOptions.newBuilder() + .setProjectId("[PROJECT]") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .build() + .getService(); + } + + @After + public void tearDown() throws Exception { + spanner.close(); + mockSpanner.reset(); + } + + @Test + public void testInlinedBeginTx() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxAborted() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + final AtomicBoolean firstAttempt = new AtomicBoolean(true); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + long res = transaction.executeUpdate(UPDATE_STATEMENT); + if (firstAttempt.getAndSet(false)) { + mockSpanner.abortTransaction(transaction); + } + return res; + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(2); + // We have started 2 transactions, because the first transaction aborted during the commit. + assertThat(countRequests(CommitRequest.class)).isEqualTo(2); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxWithQuery() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + }); + assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithRead() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try (ResultSet rs = + transaction.read("FOO", KeySet.all(), Arrays.asList("ID"))) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + }); + assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ReadRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithBatchDml() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long[] updateCounts = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + assertThat(updateCounts).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteBatchDmlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + // The transaction will be retried because the first statement that also tried to include the + // BeginTransaction statement failed and did not return a transaction. That forces a retry of + // the entire transaction with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + // The update statement will be executed 3 times: + // 1. The invalid update statement will be executed during the first attempt and fail. The + // second update statement will not be executed, as the transaction runner sees that the initial + // statement failed and did not return a valid transaction id. + // 2. The invalid update statement is executed again during the retry. + // 3. The valid update statement is only executed after the first statement succeeded. + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(3); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxWithErrorOnFirstStatement_andThenErrorOnBeginTransaction() { + mockSpanner.setBeginTransactionExecutionTime( + SimulatedExecutionTime.ofException( + Status.INTERNAL + .withDescription("Begin transaction failed due to an internal error") + .asRuntimeException())); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try { + transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return null; + } + }); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + assertThat(e.getMessage()).contains("Begin transaction failed due to an internal error"); + } + // The transaction will be retried because the first statement that also tried to include the + // BeginTransaction statement failed and did not return a transaction. That forces a retry of + // the entire transaction with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(0); + // The explicit BeginTransaction RPC failed, so only one transaction was started. + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithUncaughtError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + // No rollback request will be initiated because the client does not receive any transaction id. + assertThat(countRequests(RollbackRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithUncaughtErrorAfterSuccessfulBegin() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + // This statement will start a transaction. + transaction.executeUpdate(UPDATE_STATEMENT); + // This statement will fail and cause a rollback as the exception is not caught. + return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(2); + assertThat(countRequests(RollbackRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxBatchDmlWithErrorOnFirstStatement() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + Void res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try { + transaction.batchUpdate( + ImmutableList.of(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); + fail("missing expected exception"); + } catch (SpannerBatchUpdateException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(e.getUpdateCounts()).hasLength(0); + } + return null; + } + }); + assertThat(res).isNull(); + // The first statement failed and could not return a transaction. The entire transaction is + // therefore retried with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(ExecuteBatchDmlRequest.class)).isEqualTo(2); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxBatchDmlWithErrorOnSecondStatement() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + transaction.batchUpdate( + ImmutableList.of(UPDATE_STATEMENT, INVALID_UPDATE_STATEMENT)); + fail("missing expected exception"); + // The following line is needed as the compiler does not know that this is + // unreachable. + return -1L; + } catch (SpannerBatchUpdateException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(e.getUpdateCounts()).hasLength(1); + return e.getUpdateCounts()[0]; + } + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + // Although the batch DML returned an error, that error was for the second statement. That means + // that the transaction was started by the first statement. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteBatchDmlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithErrorOnStreamingSql() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + Void res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT)) { + while (rs.next()) {} + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return null; + } + }); + assertThat(res).isNull(); + // The transaction will be retried because the first statement that also tried to include the + // BeginTransaction statement failed and did not return a transaction. That forces a retry of + // the entire transaction with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(2); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxWithErrorOnSecondPartialResultSet() { + final Statement statement = Statement.of("SELECT * FROM BROKEN_TABLE"); + RandomResultSetGenerator generator = new RandomResultSetGenerator(2); + mockSpanner.putStatementResult(StatementResult.query(statement, generator.generate())); + // The first PartialResultSet will be returned successfully, and then a DATA_LOSS exception will + // be returned. + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofStreamException(Status.DATA_LOSS.asRuntimeException(), 1)); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + Void res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(statement)) { + while (rs.next()) {} + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DATA_LOSS); + } + return null; + } + }); + assertThat(res).isNull(); + // The transaction will not be retried, as the first PartialResultSet returns the transaction + // ID, and the second fails with an error code. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithParallelQueries() { + final int numQueries = 100; + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(16); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> futures = new ArrayList<>(numQueries); + for (int i = 0; i < numQueries; i++) { + futures.add( + executor.submit( + new Callable() { + @Override + public Long call() throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + })); + } + Long res = 0L; + for (Future f : futures) { + res += f.get(); + } + return res; + } + }); + assertThat(updateCount).isEqualTo(1L * numQueries); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithOnlyMutations() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.buffer( + Arrays.asList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), + Mutation.delete("FOO", Key.of(1L)))); + return null; + } + }); + // There should be 1 call to BeginTransaction because there is no statement that we can use to + // inline the BeginTransaction call with. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTx() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + assertThat(txn.executeUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxAborted() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + boolean first = true; + while (true) { + try { + assertThat(txn.executeUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); + if (first) { + mockSpanner.abortAllTransactions(); + first = false; + } + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxWithOnlyMutations() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + txn.buffer(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxWithError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + try { + txn.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(txn.executeUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + // The first statement will fail and not return a transaction id. This will trigger a retry of + // the entire transaction, and the retry will do an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + // The first statement will start a transaction, but it will never be returned to the client as + // the update statement fails. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxWithUncaughtError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + txn.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTx() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTxAborted() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + final AtomicBoolean firstAttempt = new AtomicBoolean(true); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + ApiFuture res = txn.executeUpdateAsync(UPDATE_STATEMENT); + if (firstAttempt.getAndSet(false)) { + mockSpanner.abortTransaction(txn); + } + return res; + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // We have started 2 transactions, because the first transaction aborted. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginAsyncTxWithQuery() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + final ExecutorService queryExecutor = Executors.newSingleThreadExecutor(); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + final SettableApiFuture res = SettableApiFuture.create(); + try (AsyncResultSet rs = txn.executeQueryAsync(SELECT1)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + res.set(resultSet.getLong(0)); + default: + throw new IllegalStateException(); + } + } + }); + } + return res; + } + }, + queryExecutor); + assertThat(updateCount.get()).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + queryExecutor.shutdown(); + } + + @Test + public void testInlinedBeginAsyncTxWithBatchDml() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCounts = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext transaction) { + return transaction.batchUpdateAsync( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }, + executor); + assertThat(updateCounts.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTxWithError() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext transaction) { + transaction.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + return transaction.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + // The first statement will fail and not return a transaction id. This will trigger a retry of + // the entire transaction, and the retry will do an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginAsyncTxWithParallelQueries() + throws InterruptedException, ExecutionException { + final int numQueries = 100; + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(16); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(final TransactionContext txn) { + List> futures = new ArrayList<>(numQueries); + for (int i = 0; i < numQueries; i++) { + final SettableApiFuture res = SettableApiFuture.create(); + try (AsyncResultSet rs = txn.executeQueryAsync(SELECT1)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + res.set(resultSet.getLong(0)); + default: + throw new IllegalStateException(); + } + } + }); + } + futures.add(res); + } + return ApiFutures.transformAsync( + ApiFutures.allAsList(futures), + new ApiAsyncFunction, Long>() { + @Override + public ApiFuture apply(List input) throws Exception { + long sum = 0L; + for (Long l : input) { + sum += l; + } + return ApiFutures.immediateFuture(sum); + } + }, + MoreExecutors.directExecutor()); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(1L * numQueries); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTxWithOnlyMutations() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext transaction) { + transaction.buffer(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .get(); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTx() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + while (true) { + AsyncTransactionStep updateCount = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + CommitTimestampFuture commitTimestamp = updateCount.commitAsync(); + try { + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(commitTimestamp.get()).isNotNull(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTxAborted() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + boolean first = true; + while (true) { + try { + AsyncTransactionStep updateCount = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + if (first) { + // Abort the transaction after the statement has been executed to ensure that the + // transaction has actually been started before the test tries to abort it. + updateCount.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Long input) + throws Exception { + mockSpanner.abortAllTransactions(); + return ApiFutures.immediateFuture(null); + } + }, + MoreExecutors.directExecutor()); + first = false; + } + assertThat(updateCount.commitAsync().get()).isNotNull(); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + // The retry will use a BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTxWithOnlyMutations() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + txn.buffer(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTxWithError() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + while (true) { + try { + AsyncTransactionStep updateCount = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + } + }, + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Long input) + throws Exception { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + try { + updateCount.commitAsync().get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + private int countRequests(Class requestType) { + int count = 0; + for (AbstractMessage msg : mockSpanner.getRequests()) { + if (msg.getClass().equals(requestType)) { + count++; + } + } + return count; + } + + private int countTransactionsStarted() { + return mockSpanner.getTransactionsStarted().size(); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java index edbc7976c07..84c7185e1f1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java @@ -74,22 +74,8 @@ public void setAllowSessionReplacing(boolean allow) { } @Override - PooledSessionFuture getReadSession() { - PooledSessionFuture session = super.getReadSession(); - if (invalidateNextSession) { - session.get().delegate.close(); - session.get().setAllowReplacing(false); - awaitDeleted(session.get().delegate); - session.get().setAllowReplacing(allowReplacing); - invalidateNextSession = false; - } - session.get().setAllowReplacing(allowReplacing); - return session; - } - - @Override - PooledSessionFuture getReadWriteSession() { - PooledSessionFuture session = super.getReadWriteSession(); + PooledSessionFuture getSession() { + PooledSessionFuture session = super.getSession(); if (invalidateNextSession) { session.get().delegate.close(); session.get().setAllowReplacing(false); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 5ecf9607a49..85e935a75a7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -218,6 +218,7 @@ public PartialResultSet next() { recordCount++; currentRow++; } + builder.setResumeToken(ByteString.copyFromUtf8(String.format("%09d", currentRow))); hasNext = currentRow < resultSet.getRowsCount(); return builder.build(); } @@ -453,12 +454,14 @@ public static SimulatedExecutionTime stickyDatabaseNotFoundException(String name SpannerExceptionFactoryTest.newStatusDatabaseNotFoundException(name)); } - public static SimulatedExecutionTime ofExceptions(Collection exceptions) { + public static SimulatedExecutionTime ofExceptions(Collection exceptions) { return new SimulatedExecutionTime(0, 0, exceptions, false, Collections.emptySet()); } public static SimulatedExecutionTime ofMinimumAndRandomTimeAndExceptions( - int minimumExecutionTime, int randomExecutionTime, Collection exceptions) { + int minimumExecutionTime, + int randomExecutionTime, + Collection exceptions) { return new SimulatedExecutionTime( minimumExecutionTime, randomExecutionTime, @@ -475,7 +478,7 @@ private SimulatedExecutionTime(int minimum, int random) { private SimulatedExecutionTime( int minimum, int random, - Collection exceptions, + Collection exceptions, boolean stickyException, Collection streamIndices) { Preconditions.checkArgument(minimum >= 0, "Minimum execution time must be >= 0"); @@ -540,6 +543,7 @@ private static void checkStreamException( private ConcurrentMap sessions = new ConcurrentHashMap<>(); private ConcurrentMap sessionLastUsed = new ConcurrentHashMap<>(); private ConcurrentMap transactions = new ConcurrentHashMap<>(); + private final Queue transactionsStarted = new ConcurrentLinkedQueue<>(); private ConcurrentMap isPartitionedDmlTransaction = new ConcurrentHashMap<>(); private ConcurrentMap abortedTransactions = new ConcurrentHashMap<>(); @@ -966,14 +970,6 @@ public void executeSql(ExecuteSqlRequest request, StreamObserver resp } } - private ResultSetMetadata createTransactionMetadata(TransactionSelector transactionSelector) { - if (transactionSelector.hasBegin() || transactionSelector.hasSingleUse()) { - Transaction transaction = getTemporaryTransactionOrNull(transactionSelector); - return ResultSetMetadata.newBuilder().setTransaction(transaction).build(); - } - return ResultSetMetadata.getDefaultInstance(); - } - private void returnResultSet( ResultSet resultSet, ByteString transactionId, @@ -1068,7 +1064,10 @@ public void executeBatchDml( ResultSet.newBuilder() .setStats( ResultSetStats.newBuilder().setRowCountExact(res.getUpdateCount()).build()) - .setMetadata(createTransactionMetadata(request.getTransaction())) + .setMetadata( + ResultSetMetadata.newBuilder() + .setTransaction(Transaction.newBuilder().setId(transactionId).build()) + .build()) .build()); } builder.setStatus(status); @@ -1645,6 +1644,7 @@ private Transaction beginTransaction(Session session, TransactionOptions options } Transaction transaction = builder.build(); transactions.put(transaction.getId(), transaction); + transactionsStarted.add(transaction.getId()); isPartitionedDmlTransaction.put( transaction.getId(), options.getModeCase() == ModeCase.PARTITIONED_DML); if (abortNextTransaction.getAndSet(false)) { @@ -1917,6 +1917,10 @@ public void waitForLastRequestToBe(Class type, long t } } + public List getTransactionsStarted() { + return new ArrayList<>(transactionsStarted); + } + public void waitForRequestsToContain(Class type, long timeoutMillis) throws InterruptedException, TimeoutException { Stopwatch watch = Stopwatch.createStarted(); @@ -1978,6 +1982,7 @@ public void reset() { sessions = new ConcurrentHashMap<>(); sessionLastUsed = new ConcurrentHashMap<>(); transactions = new ConcurrentHashMap<>(); + transactionsStarted.clear(); isPartitionedDmlTransaction = new ConcurrentHashMap<>(); abortedTransactions = new ConcurrentHashMap<>(); transactionCounters = new ConcurrentHashMap<>(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java index 475d8325a98..50cf96ff3c3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java @@ -47,7 +47,7 @@ private static class NoOpListener implements AbstractResultSet.Listener { public void onTransactionMetadata(Transaction transaction) throws SpannerException {} @Override - public void onError(SpannerException e) {} + public void onError(SpannerException e, boolean withBeginTransaction) {} @Override public void onDone() {} @@ -119,7 +119,7 @@ public void cancel(@Nullable String message) {} public void request(int numMessages) {} }); consumer = stream.consumer(); - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); JSONArray chunks = testCase.getJSONArray("chunks"); JSONObject expectedResult = testCase.getJSONObject("result"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java new file mode 100644 index 00000000000..4690a30aa71 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java @@ -0,0 +1,540 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.gax.grpc.testing.LocalChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ListValue; +import com.google.spanner.v1.BeginTransactionRequest; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +import com.google.spanner.v1.TypeCode; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessServerBuilder; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ReadWriteTransactionWithInlineBeginTest { + private static MockSpannerServiceImpl mockSpanner; + private static Server server; + private static LocalChannelProvider channelProvider; + private static final Statement UPDATE_STATEMENT = + Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2"); + private static final Statement INVALID_UPDATE_STATEMENT = + Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2"); + private static final Statement INVALID_SELECT_STATEMENT = + Statement.of("SELECT * FROM NON_EXISTENT_TABLE"); + private static final long UPDATE_COUNT = 1L; + private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); + private static final ResultSetMetadata SELECT1_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("COL1") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.INT64) + .build()) + .build()) + .build()) + .build(); + private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build()) + .build()) + .setMetadata(SELECT1_METADATA) + .build(); + private Spanner spanner; + private DatabaseClient client; + + @BeforeClass + public static void startStaticServer() throws IOException { + mockSpanner = new MockSpannerServiceImpl(); + mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET)); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_UPDATE_STATEMENT, + Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_SELECT_STATEMENT, + Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); + + String uniqueName = InProcessServerBuilder.generateName(); + server = + InProcessServerBuilder.forName(uniqueName) + // We need to use a real executor for timeouts to occur. + .scheduledExecutorService(new ScheduledThreadPoolExecutor(1)) + .addService(mockSpanner) + .build() + .start(); + channelProvider = LocalChannelProvider.create(uniqueName); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + } + + @Before + public void setUp() throws IOException { + mockSpanner.reset(); + mockSpanner.removeAllExecutionTimes(); + spanner = + SpannerOptions.newBuilder() + .setProjectId("[PROJECT]") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .build() + .getService(); + client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + } + + @After + public void tearDown() throws Exception { + spanner.close(); + } + + @Test + public void singleUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void singleBatchUpdate() { + long[] updateCounts = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + assertThat(updateCounts).isEqualTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void singleQuery() { + Long value = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + }); + assertThat(value).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void updateAndQuery() { + long[] res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + long updateCount = transaction.executeUpdate(UPDATE_STATEMENT); + long val = 0L; + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + val = rs.getLong(0); + } + } + return new long[] {updateCount, val}; + } + }); + assertThat(res).isEqualTo(new long[] {UPDATE_COUNT, 1L}); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void concurrentUpdates() { + final int updates = 100; + final ExecutorService service = Executors.newFixedThreadPool(8); + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> list = new ArrayList<>(updates); + for (int i = 0; i < updates; i++) { + list.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + return transaction.executeUpdate(UPDATE_STATEMENT); + } + })); + } + long totalUpdateCount = 0L; + for (Future fut : list) { + totalUpdateCount += fut.get(); + } + return totalUpdateCount; + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT * updates); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void concurrentBatchUpdates() { + final int updates = 100; + final ExecutorService service = Executors.newFixedThreadPool(8); + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> list = new ArrayList<>(updates); + for (int i = 0; i < updates; i++) { + list.add( + service.submit( + new Callable() { + @Override + public long[] call() throws Exception { + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + })); + } + long totalUpdateCount = 0L; + for (Future fut : list) { + for (long l : fut.get()) { + totalUpdateCount += l; + } + } + return totalUpdateCount; + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT * updates * 2); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void concurrentQueries() { + final int queries = 100; + final ExecutorService service = Executors.newFixedThreadPool(8); + Long selectedTotal = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> list = new ArrayList<>(queries); + for (int i = 0; i < queries; i++) { + list.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + })); + } + long selectedTotal = 0L; + for (Future fut : list) { + selectedTotal += fut.get(); + } + return selectedTotal; + } + }); + assertThat(selectedTotal).isEqualTo(queries); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void failedUpdate() { + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void failedBatchUpdate() { + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + return transaction.batchUpdate( + Arrays.asList(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void failedQuery() { + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { + rs.next(); + } + return null; + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void failedUpdateAndThenUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + // This update statement carries the BeginTransaction, but fails. This will + // cause the entire transaction to be retried with an explicit + // BeginTransaction RPC to ensure all statements in the transaction are + // actually executed against the same transaction. + transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void failedBatchUpdateAndThenUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + // This update statement carries the BeginTransaction, but fails. This will + // cause the entire transaction to be retried with an explicit + // BeginTransaction RPC to ensure all statements in the transaction are + // actually executed against the same transaction. + transaction.batchUpdate( + Arrays.asList(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void failedQueryAndThenUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + // This query carries the BeginTransaction, but fails. The BeginTransaction will + // then be carried by the subsequent statement. + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { + rs.next(); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void abortedUpdate() { + final AtomicInteger attempt = new AtomicInteger(); + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + if (attempt.incrementAndGet() == 1) { + // We use abortNextTransaction here, as the transaction context does not yet + // have a transaction (it will be requested by the first update statement). + mockSpanner.abortNextTransaction(); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(attempt.get()).isEqualTo(2); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void abortedBatchUpdate() { + final AtomicInteger attempt = new AtomicInteger(); + long[] updateCounts = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + if (attempt.incrementAndGet() == 1) { + // We use abortNextTransaction here, as the transaction context does not yet + // have a transaction (it will be requested by the first update statement). + mockSpanner.abortNextTransaction(); + } + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + assertThat(updateCounts).isEqualTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}); + assertThat(attempt.get()).isEqualTo(2); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + private int countRequests(Class requestType) { + int count = 0; + for (AbstractMessage msg : mockSpanner.getRequests()) { + if (msg.getClass().equals(requestType)) { + count++; + } + } + return count; + } + + private int countTransactionsStarted() { + return mockSpanner.getTransactionsStarted().size(); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java index fcf1c6e35bc..5e732c1eabb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java @@ -217,17 +217,6 @@ public void tearDown() { spanner.close(); } - private static void initReadWriteSessionPool() throws InterruptedException { - // Wait for at least one read/write session to be ready. - Stopwatch watch = Stopwatch.createStarted(); - while (((DatabaseClientImpl) client).pool.getNumberOfAvailableWritePreparedSessions() == 0) { - if (watch.elapsed(TimeUnit.SECONDS) > 5L) { - fail("No read/write sessions prepared"); - } - Thread.sleep(5L); - } - } - private static void invalidateSessionPool() throws InterruptedException { invalidateSessionPool(client, spanner.getOptions().getSessionPoolOptions().getMinSessions()); } @@ -576,16 +565,10 @@ public void readOnlyTransactionReadRowUsingIndexNonRecoverable() throws Interrup } } - /** - * Test with one read-only session in the pool that is invalidated. The session pool will try to - * prepare this session for read/write, which will fail with a {@link SessionNotFoundException}. - * That again will trigger the creation of a new session. This will always succeed. - */ @Test public void readWriteTransactionReadOnlySessionInPool() throws InterruptedException { // Create a session pool with only read sessions. - SessionPoolOptions.Builder builder = - SessionPoolOptions.newBuilder().setWriteSessionsFraction(0.0f); + SessionPoolOptions.Builder builder = SessionPoolOptions.newBuilder(); if (failOnInvalidatedSession) { builder.setFailIfSessionNotFound(); } @@ -600,27 +583,31 @@ public void readWriteTransactionReadOnlySessionInPool() throws InterruptedExcept DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); invalidateSessionPool(client, spanner.getOptions().getSessionPoolOptions().getMinSessions()); - TransactionRunner runner = client.readWriteTransaction(); - int count = - runner.run( - new TransactionCallable() { - @Override - public Integer run(TransactionContext transaction) { - int count = 0; - try (ResultSet rs = transaction.executeQuery(SELECT1AND2)) { - while (rs.next()) { - count++; + try { + TransactionRunner runner = client.readWriteTransaction(); + int count = + runner.run( + new TransactionCallable() { + @Override + public Integer run(TransactionContext transaction) { + int count = 0; + try (ResultSet rs = transaction.executeQuery(SELECT1AND2)) { + while (rs.next()) { + count++; + } } + return count; } - return count; - } - }); - assertThat(count).isEqualTo(2); + }); + assertThat(count).isEqualTo(2); + assertThat(failOnInvalidatedSession).isFalse(); + } catch (SessionNotFoundException e) { + assertThat(failOnInvalidatedSession).isTrue(); + } } @Test public void readWriteTransactionSelect() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -647,7 +634,6 @@ public Integer run(TransactionContext transaction) { @Test public void readWriteTransactionRead() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -674,7 +660,6 @@ public Integer run(TransactionContext transaction) { @Test public void readWriteTransactionReadUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -703,7 +688,6 @@ public Integer run(TransactionContext transaction) { @Test public void readWriteTransactionReadRow() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -724,7 +708,6 @@ public Struct run(TransactionContext transaction) { @Test public void readWriteTransactionReadRowUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -746,7 +729,6 @@ public Struct run(TransactionContext transaction) { @Test public void readWriteTransactionUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -767,7 +749,6 @@ public Long run(TransactionContext transaction) { @Test public void readWriteTransactionBatchUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -789,7 +770,6 @@ public long[] run(TransactionContext transaction) { @Test public void readWriteTransactionBuffer() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -1022,14 +1002,16 @@ public void transactionManagerReadOnlySessionInPool() throws InterruptedExceptio transaction = manager.resetForRetry(); } } + assertThat(count).isEqualTo(2); + assertThat(failOnInvalidatedSession).isFalse(); + } catch (SessionNotFoundException e) { + assertThat(failOnInvalidatedSession).isTrue(); } - assertThat(count).isEqualTo(2); } @SuppressWarnings("resource") @Test public void transactionManagerSelect() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { int count = 0; @@ -1058,7 +1040,6 @@ public void transactionManagerSelect() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerRead() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { int count = 0; @@ -1087,7 +1068,6 @@ public void transactionManagerRead() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerReadUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { int count = 0; @@ -1117,7 +1097,6 @@ public void transactionManagerReadUsingIndex() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerReadRow() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { Struct row; @@ -1142,7 +1121,6 @@ public void transactionManagerReadRow() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerReadRowUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { Struct row; @@ -1167,7 +1145,6 @@ public void transactionManagerReadRowUsingIndex() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { long count; @@ -1192,7 +1169,6 @@ public void transactionManagerUpdate() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerBatchUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { long[] count; @@ -1218,7 +1194,6 @@ public void transactionManagerBatchUpdate() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerBuffer() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { TransactionContext transaction = manager.begin(); @@ -1417,7 +1392,6 @@ public void transactionManagerReadRowUsingIndexInvalidatedDuringTransaction() @Test public void partitionedDml() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { assertThat(client.executePartitionedUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); @@ -1429,7 +1403,6 @@ public void partitionedDml() throws InterruptedException { @Test public void write() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { Timestamp timestamp = client.write(Arrays.asList(Mutation.delete("FOO", KeySet.all()))); @@ -1442,7 +1415,6 @@ public void write() throws InterruptedException { @Test public void writeAtLeastOnce() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { Timestamp timestamp = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java index c756a7898ad..3827b2a2804 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java @@ -31,6 +31,7 @@ import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; import com.google.protobuf.ListValue; import com.google.protobuf.util.Timestamps; import com.google.spanner.v1.BeginTransactionRequest; @@ -40,6 +41,7 @@ import com.google.spanner.v1.PartialResultSet; import com.google.spanner.v1.ReadRequest; import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.Session; import com.google.spanner.v1.Transaction; import io.opencensus.trace.Span; @@ -85,6 +87,7 @@ public void setUp() { GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class); when(transportOptions.getExecutorFactory()).thenReturn(mock(ExecutorFactory.class)); when(spannerOptions.getTransportOptions()).thenReturn(transportOptions); + when(spannerOptions.getSessionPoolOptions()).thenReturn(mock(SessionPoolOptions.class)); @SuppressWarnings("resource") SpannerImpl spanner = new SpannerImpl(rpc, spannerOptions); String dbName = "projects/p1/instances/i1/databases/d1"; @@ -109,6 +112,8 @@ public void setUp() { .build(); Mockito.when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.any(Map.class))) .thenReturn(ApiFutures.immediateFuture(commitResponse)); + Mockito.when(rpc.rollbackAsync(Mockito.any(RollbackRequest.class), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); session = spanner.getSessionClient(db).createSession(); ((SessionImpl) session).setCurrentSpan(mock(Span.class)); // We expect the same options, "options", on all calls on "session". diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java index 2dc31bb28a9..f559a04b94c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java @@ -80,15 +80,10 @@ public void setUp() { .setProjectId("[PROJECT]") .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()); - // Make sure the session pool is empty by default, does not contain any write-prepared sessions, + // Make sure the session pool is empty by default, does not contain any sessions, // contains at most 2 sessions, and creates sessions in steps of 1. builder.setSessionPoolOption( - SessionPoolOptions.newBuilder() - .setMinSessions(0) - .setMaxSessions(2) - .setIncStep(1) - .setWriteSessionsFraction(0.0f) - .build()); + SessionPoolOptions.newBuilder().setMinSessions(0).setMaxSessions(2).setIncStep(1).build()); spanner = builder.build().getService(); client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); pool = ((DatabaseClientImpl) client).pool; @@ -162,15 +157,15 @@ public void run() { @Test public void testTransactionManagerExceptionOnBegin() { - transactionManagerTest( - new Runnable() { - @Override - public void run() { - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofException(FAILED_PRECONDITION)); - } - }, - 1); + assertThat(pool.getNumberOfSessionsInPool(), is(equalTo(0))); + mockSpanner.setBeginTransactionExecutionTime( + SimulatedExecutionTime.ofException(FAILED_PRECONDITION)); + try (TransactionManager txManager = client.transactionManager()) { + // This should not cause an error, as the actual BeginTransaction will be included with the + // first statement of the transaction. + txManager.begin(); + } + assertThat(pool.getNumberOfSessionsInPool(), is(equalTo(1))); } private void transactionManagerTest(Runnable setup, int expectedNumberOfSessionsAfterExecution) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java index 0e72b2b9bc5..0c965a55730 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java @@ -150,8 +150,8 @@ public void testKeepAlive() throws Exception { // Checkout two sessions and do a maintenance loop. Still no sessions should be getting any // pings. - Session session1 = pool.getReadSession(); - Session session2 = pool.getReadSession(); + Session session1 = pool.getSession(); + Session session2 = pool.getSession(); runMaintainanceLoop(clock, pool, 1); assertThat(pingedSessions).isEmpty(); @@ -173,9 +173,9 @@ public void testKeepAlive() throws Exception { // Now check out three sessions so the pool will create an additional session. The pool will // only keep 2 sessions alive, as that is the setting for MinSessions. - Session session3 = pool.getReadSession(); - Session session4 = pool.getReadSession(); - Session session5 = pool.getReadSession(); + Session session3 = pool.getSession(); + Session session4 = pool.getSession(); + Session session5 = pool.getSession(); // Note that session2 was now the first session in the pool as it was the last to receive a // ping. assertThat(session3.getName()).isEqualTo(session2.getName()); @@ -192,7 +192,7 @@ public void testKeepAlive() throws Exception { // should cause only one session to get a ping. clock.currentTimeMillis += TimeUnit.MINUTES.toMillis(options.getKeepAliveIntervalMinutes()) + 1; // We are now checking out session2 because - Session session6 = pool.getReadSession(); + Session session6 = pool.getSession(); // The session that was first in the pool now is equal to the initial first session as each full // round of pings will swap the order of the first MinSessions sessions in the pool. assertThat(session6.getName()).isEqualTo(session1.getName()); @@ -208,9 +208,9 @@ public void testKeepAlive() throws Exception { // Now check out 3 sessions again and make sure the 'extra' session is checked in last. That // will make it eligible for pings. - Session session7 = pool.getReadSession(); - Session session8 = pool.getReadSession(); - Session session9 = pool.getReadSession(); + Session session7 = pool.getSession(); + Session session8 = pool.getSession(); + Session session9 = pool.getSession(); assertThat(session7.getName()).isEqualTo(session1.getName()); assertThat(session8.getName()).isEqualTo(session2.getName()); @@ -244,8 +244,8 @@ public void testIdleSessions() throws Exception { assertThat(idledSessions).isEmpty(); // Checkout two sessions and do a maintenance loop. Still no sessions should be removed. - Session session1 = pool.getReadSession(); - Session session2 = pool.getReadSession(); + Session session1 = pool.getSession(); + Session session2 = pool.getSession(); runMaintainanceLoop(clock, pool, 1); assertThat(idledSessions).isEmpty(); @@ -262,9 +262,9 @@ public void testIdleSessions() throws Exception { // Now check out three sessions so the pool will create an additional session. The pool will // only keep 2 sessions alive, as that is the setting for MinSessions. - Session session3 = pool.getReadSession().get(); - Session session4 = pool.getReadSession().get(); - Session session5 = pool.getReadSession().get(); + Session session3 = pool.getSession().get(); + Session session4 = pool.getSession().get(); + Session session5 = pool.getSession().get(); // Note that session2 was now the first session in the pool as it was the last to receive a // ping. assertThat(session3.getName()).isEqualTo(session2.getName()); @@ -279,9 +279,9 @@ public void testIdleSessions() throws Exception { assertThat(pool.totalSessions()).isEqualTo(2); // Check out three sessions again and keep one session checked out. - Session session6 = pool.getReadSession().get(); - Session session7 = pool.getReadSession().get(); - Session session8 = pool.getReadSession().get(); + Session session6 = pool.getSession().get(); + Session session7 = pool.getSession().get(); + Session session8 = pool.getSession().get(); session8.close(); session7.close(); // Now advance the clock to idle sessions. This should remove session8 from the pool. @@ -293,9 +293,9 @@ public void testIdleSessions() throws Exception { // Check out three sessions and keep them all checked out. No sessions should be removed from // the pool. - Session session9 = pool.getReadSession().get(); - Session session10 = pool.getReadSession().get(); - Session session11 = pool.getReadSession().get(); + Session session9 = pool.getSession().get(); + Session session10 = pool.getSession().get(); + Session session11 = pool.getSession().get(); runMaintainanceLoop(clock, pool, loopsToIdleSessions); assertThat(idledSessions).containsExactly(session5, session8); assertThat(pool.totalSessions()).isEqualTo(3); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java index b806f5fad6b..a3b2a3c5425 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java @@ -205,15 +205,6 @@ private void expireSession(Session session) { } } - private void assertWritePrepared(Session session) { - String name = session.getName(); - synchronized (lock) { - if (!sessions.containsKey(name) || !sessions.get(name)) { - setFailed(); - } - } - } - private void resetTransaction(SessionImpl session) { String name = session.getName(); synchronized (lock) { @@ -242,7 +233,6 @@ public void stressTest() throws Exception { final int numOperationsPerThread = 1000; final CountDownLatch releaseThreads = new CountDownLatch(1); final CountDownLatch threadsDone = new CountDownLatch(concurrentThreads); - final int writeOperationFraction = 5; setupSpanner(db); int minSessions = 2; int maxSessions = concurrentThreads / 2; @@ -280,15 +270,8 @@ public void run() { Uninterruptibles.awaitUninterruptibly(releaseThreads); for (int j = 0; j < numOperationsPerThread; j++) { try { - PooledSessionFuture session = null; - if (random.nextInt(10) < writeOperationFraction) { - session = pool.getReadWriteSession(); - PooledSession sess = session.get(); - assertWritePrepared(sess); - } else { - session = pool.getReadSession(); - session.get(); - } + PooledSessionFuture session = pool.getSession(); + session.get(); Uninterruptibles.sleepUninterruptibly( random.nextInt(5), TimeUnit.MILLISECONDS); resetTransaction(session.get().delegate); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java index d5ea648bbd0..0620bfb0e98 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java @@ -54,7 +54,6 @@ import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc.ResultStreamConsumer; -import com.google.common.base.Stopwatch; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.Uninterruptibles; import com.google.protobuf.ByteString; @@ -180,7 +179,7 @@ public void testClosedPoolIncludesClosedException() { assertThat(pool.isValid()).isTrue(); closePoolWithStacktrace(); try { - pool.getReadSession(); + pool.getSession(); fail("missing expected exception"); } catch (IllegalStateException e) { assertThat(e.getCause()).isInstanceOf(ClosedException.class); @@ -198,7 +197,7 @@ private void closePoolWithStacktrace() { public void sessionCreation() { setupMockSessionCreation(); pool = createPool(); - try (Session session = pool.getReadSession()) { + try (Session session = pool.getSession()) { assertThat(session).isNotNull(); } } @@ -207,25 +206,18 @@ public void sessionCreation() { public void poolLifo() { setupMockSessionCreation(); pool = createPool(); - Session session1 = pool.getReadSession().get(); - Session session2 = pool.getReadSession().get(); + Session session1 = pool.getSession().get(); + Session session2 = pool.getSession().get(); assertThat(session1).isNotEqualTo(session2); session2.close(); session1.close(); - Session session3 = pool.getReadSession().get(); - Session session4 = pool.getReadSession().get(); + Session session3 = pool.getSession().get(); + Session session4 = pool.getSession().get(); assertThat(session3).isEqualTo(session1); assertThat(session4).isEqualTo(session2); session3.close(); session4.close(); - - Session session5 = pool.getReadWriteSession().get(); - Session session6 = pool.getReadWriteSession().get(); - assertThat(session5).isEqualTo(session4); - assertThat(session6).isEqualTo(session3); - session6.close(); - session5.close(); } @Test @@ -260,9 +252,9 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - Session session1 = pool.getReadSession(); + Session session1 = pool.getSession(); // Leaked sessions - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); // Clear the leaked exception to suppress logging of expected exceptions. leakedSession.clearLeakedException(); session1.close(); @@ -338,7 +330,7 @@ public Void call() throws Exception { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); AtomicBoolean failed = new AtomicBoolean(false); @@ -396,12 +388,12 @@ public Void call() throws Exception { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); AtomicBoolean failed = new AtomicBoolean(false); CountDownLatch latch = new CountDownLatch(1); - getReadWriteSessionAsync(latch, failed); + getSessionAsync(latch, failed); insideCreation.await(); pool.closeAsync(new SpannerImpl.ClosedException()); releaseCreation.countDown(); @@ -446,51 +438,6 @@ public Void call() throws Exception { assertThat(f.isDone()).isTrue(); } - @Test - public void poolClosesEvenIfPreparationFails() throws Exception { - final SessionImpl session = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(session); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - final CountDownLatch insidePrepare = new CountDownLatch(1); - final CountDownLatch releasePrepare = new CountDownLatch(1); - doAnswer( - new Answer() { - @Override - public Session answer(InvocationOnMock invocation) throws Throwable { - insidePrepare.countDown(); - releasePrepare.await(); - throw SpannerExceptionFactory.newSpannerException(new RuntimeException()); - } - }) - .when(session) - .prepareReadWriteTransaction(); - pool = createPool(); - AtomicBoolean failed = new AtomicBoolean(false); - CountDownLatch latch = new CountDownLatch(1); - getReadWriteSessionAsync(latch, failed); - insidePrepare.await(); - ListenableFuture f = pool.closeAsync(new SpannerImpl.ClosedException()); - releasePrepare.countDown(); - f.get(); - assertThat(f.isDone()).isTrue(); - } - @Test public void poolClosureFailsNewRequests() { final SessionImpl session = mockSession(); @@ -513,13 +460,13 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); leakedSession.get(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); pool.closeAsync(new SpannerImpl.ClosedException()); try { - pool.getReadSession(); + pool.getSession(); fail("Expected exception"); } catch (IllegalStateException ex) { assertNotNull(ex.getMessage()); @@ -566,283 +513,13 @@ public Void call() { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); try { - pool.getReadSession().get(); - fail("Expected exception"); - } catch (SpannerException ex) { - assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); - } - } - - @Test - public void creationExceptionPropagatesToReadWriteSession() { - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Callable() { - @Override - public Void call() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionCreateFailure( - SpannerExceptionFactory.newSpannerException(ErrorCode.INTERNAL, ""), 1); - return null; - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - try { - pool.getReadWriteSession().get(); + pool.getSession().get(); fail("Expected exception"); } catch (SpannerException ex) { assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); } } - @Test - public void prepareExceptionPropagatesToReadWriteSession() { - final SessionImpl session = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(session); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.INTERNAL, "")) - .when(session) - .prepareReadWriteTransaction(); - pool = createPool(); - try { - pool.getReadWriteSession().get(); - fail("Expected exception"); - } catch (SpannerException ex) { - assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); - } - } - - @Test - public void getReadWriteSession() { - final SessionImpl mockSession = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(mockSession); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - try (PooledSessionFuture session = pool.getReadWriteSession()) { - assertThat(session).isNotNull(); - session.get(); - verify(mockSession).prepareReadWriteTransaction(); - } - } - - @Test - public void getMultipleReadWriteSessions() throws Exception { - SessionImpl mockSession1 = mockSession(); - SessionImpl mockSession2 = mockSession(); - final LinkedList sessions = - new LinkedList<>(Arrays.asList(mockSession1, mockSession2)); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(sessions.pop()); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - PooledSessionFuture session1 = pool.getReadWriteSession(); - PooledSessionFuture session2 = pool.getReadWriteSession(); - session1.get(); - session2.get(); - verify(mockSession1).prepareReadWriteTransaction(); - verify(mockSession2).prepareReadWriteTransaction(); - session1.close(); - session2.close(); - } - - @Test - public void getMultipleConcurrentReadWriteSessions() { - AtomicBoolean failed = new AtomicBoolean(false); - final SessionImpl session = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(session); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - - pool = createPool(); - int numSessions = 5; - final CountDownLatch latch = new CountDownLatch(numSessions); - for (int i = 0; i < numSessions; i++) { - getReadWriteSessionAsync(latch, failed); - } - Uninterruptibles.awaitUninterruptibly(latch); - } - - @Test - public void sessionIsPrePrepared() { - final SessionImpl mockSession1 = mockSession(); - final SessionImpl mockSession2 = mockSession(); - final CountDownLatch prepareLatch = new CountDownLatch(1); - doAnswer( - new Answer() { - - @Override - public Void answer(InvocationOnMock arg0) { - prepareLatch.countDown(); - return null; - } - }) - .when(mockSession1) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - - @Override - public Void answer(InvocationOnMock arg0) { - prepareLatch.countDown(); - return null; - } - }) - .when(mockSession2) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(mockSession1); - consumer.onSessionReady(mockSession2); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(2), Mockito.anyBoolean(), any(SessionConsumer.class)); - - options = - SessionPoolOptions.newBuilder() - .setMinSessions(2) - .setMaxSessions(2) - .setWriteSessionsFraction(0.5f) - .build(); - pool = createPool(); - // One of the sessions would be pre prepared. - Uninterruptibles.awaitUninterruptibly(prepareLatch); - PooledSession readSession = pool.getReadSession().get(); - PooledSession writeSession = pool.getReadWriteSession().get(); - verify(writeSession.delegate, times(1)).prepareReadWriteTransaction(); - verify(readSession.delegate, never()).prepareReadWriteTransaction(); - readSession.close(); - writeSession.close(); - } - - @Test - public void getReadSessionFallsBackToWritePreparedSession() throws Exception { - final SessionImpl mockSession1 = mockSession(); - final CountDownLatch prepareLatch = new CountDownLatch(2); - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock arg0) { - prepareLatch.countDown(); - return null; - } - }) - .when(mockSession1) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(mockSession1); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - options = - SessionPoolOptions.newBuilder() - .setMinSessions(minSessions) - .setMaxSessions(1) - .setWriteSessionsFraction(1.0f) - .build(); - pool = createPool(); - pool.getReadWriteSession().close(); - prepareLatch.await(); - // This session should also be write prepared. - PooledSession readSession = pool.getReadSession().get(); - verify(readSession.delegate, times(2)).prepareReadWriteTransaction(); - } - @Test public void failOnPoolExhaustion() { options = @@ -870,50 +547,19 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - Session session1 = pool.getReadSession(); + Session session1 = pool.getSession(); try { - pool.getReadSession(); + pool.getSession(); fail("Expected exception"); } catch (SpannerException ex) { assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED); } session1.close(); - session1 = pool.getReadSession(); + session1 = pool.getSession(); assertThat(session1).isNotNull(); session1.close(); } - @Test - public void poolWorksWhenSessionNotFound() { - SessionImpl mockSession1 = mockSession(); - SessionImpl mockSession2 = mockSession(); - final LinkedList sessions = - new LinkedList<>(Arrays.asList(mockSession1, mockSession2)); - doThrow(SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName)) - .when(mockSession1) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(sessions.pop()); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - assertThat(pool.getReadWriteSession().get().delegate).isEqualTo(mockSession2); - } - @Test public void idleSessionCleanup() throws Exception { options = @@ -953,12 +599,12 @@ public void run() { clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); // Make sure pool has been initialized - pool.getReadSession().close(); + pool.getSession().close(); runMaintainanceLoop(clock, pool, pool.poolMaintainer.numClosureCycles); assertThat(pool.numIdleSessionsRemoved()).isEqualTo(0L); - PooledSessionFuture readSession1 = pool.getReadSession(); - PooledSessionFuture readSession2 = pool.getReadSession(); - PooledSessionFuture readSession3 = pool.getReadSession(); + PooledSessionFuture readSession1 = pool.getSession(); + PooledSessionFuture readSession2 = pool.getSession(); + PooledSessionFuture readSession3 = pool.getSession(); // Wait until the sessions have actually been gotten in order to make sure they are in use in // parallel. readSession1.get(); @@ -973,9 +619,9 @@ public void run() { assertThat(pool.numIdleSessionsRemoved()).isEqualTo(0L); // Counters have now been reset // Use all 3 sessions sequentially - pool.getReadSession().close(); - pool.getReadSession().close(); - pool.getReadSession().close(); + pool.getSession().close(); + pool.getSession().close(); + pool.getSession().close(); // Advance the time by running the maintainer. This should cause // one session to be kept alive and two sessions to be removed. long cycles = @@ -1017,8 +663,8 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - PooledSessionFuture session1 = pool.getReadSession(); - PooledSessionFuture session2 = pool.getReadSession(); + PooledSessionFuture session1 = pool.getSession(); + PooledSessionFuture session2 = pool.getSession(); session1.get(); session2.get(); session1.close(); @@ -1029,7 +675,7 @@ public void run() { verify(session, times(2)).singleUse(any(TimestampBound.class)); clock.currentTimeMillis += clock.currentTimeMillis + (options.getKeepAliveIntervalMinutes() + 5) * 60 * 1000; - session1 = pool.getReadSession(); + session1 = pool.getSession(); session1.writeAtLeastOnce(new ArrayList()); session1.close(); runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); @@ -1040,156 +686,53 @@ public void run() { } @Test - public void testMaintainerKeepsWriteProportion() throws Exception { + public void blockAndTimeoutOnPoolExhaustion() throws Exception { + // Create a session pool with max 1 session and a low timeout for waiting for a session. options = SessionPoolOptions.newBuilder() - .setMinSessions(10) - .setMaxSessions(20) - .setWriteSessionsFraction(0.5f) + .setMinSessions(minSessions) + .setMaxSessions(1) + .setInitialWaitForSessionTimeoutMillis(20L) .build(); - final SessionImpl session = mockSession(); - mockKeepAlive(session); - // This is cheating as we are returning the same session each but it makes the verification - // easier. - doAnswer( - new Answer() { + setupMockSessionCreation(); + pool = createPool(); + // Take the only session that can be in the pool. + PooledSessionFuture checkedOutSession = pool.getSession(); + checkedOutSession.get(); + ExecutorService executor = Executors.newFixedThreadPool(1); + final CountDownLatch latch = new CountDownLatch(1); + // Then try asynchronously to take another session. This attempt should time out. + Future fut = + executor.submit( + new Callable() { @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - int sessionCount = invocation.getArgumentAt(0, Integer.class); - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - for (int i = 0; i < sessionCount; i++) { - consumer.onSessionReady(session); - } - } - }); + public Void call() { + latch.countDown(); + PooledSessionFuture session = pool.getSession(); + session.close(); return null; } - }) - .when(sessionClient) - .asyncBatchCreateSessions(anyInt(), Mockito.anyBoolean(), any(SessionConsumer.class)); - FakeClock clock = new FakeClock(); - clock.currentTimeMillis = System.currentTimeMillis(); - pool = createPool(clock); - // Wait until all sessions have been created and prepared. - waitForExpectedSessionPool(options.getMinSessions(), options.getWriteSessionsFraction()); - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMinSessions()); - assertThat(pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo((int) Math.ceil(options.getMinSessions() * options.getWriteSessionsFraction())); - - // Run maintainer numKeepAliveCycles. No pings should be executed during these. - runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); - verify(session, never()).singleUse(any(TimestampBound.class)); - // Run maintainer numKeepAliveCycles again. All sessions should now be pinged. - runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); - verify(session, times(options.getMinSessions())).singleUse(any(TimestampBound.class)); - // Verify that all sessions are still in the pool, and that the write fraction is maintained. - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMinSessions()); - assertThat(pool.getNumberOfWriteSessionsInPool()) - .isEqualTo( - (int) Math.ceil(pool.getNumberOfSessionsInPool() * options.getWriteSessionsFraction())); - - // Check out MaxSessions sessions to add additional sessions to the pool. - List sessions = new ArrayList<>(options.getMaxSessions()); - for (int i = 0; i < options.getMaxSessions(); i++) { - sessions.add(pool.getReadSession()); - } - for (Session s : sessions) { - s.close(); - } - // There should be MaxSessions in the pool and the writeFraction should be respected. - waitForExpectedSessionPool(options.getMaxSessions(), options.getWriteSessionsFraction()); - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMaxSessions()); - assertThat(pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo((int) Math.ceil(options.getMaxSessions() * options.getWriteSessionsFraction())); - - // Advance the clock to allow the sessions to time out or be kept alive. - clock.currentTimeMillis += - clock.currentTimeMillis + (options.getKeepAliveIntervalMinutes() + 5) * 60 * 1000; - runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); - // The session pool only keeps MinSessions alive. - verify(session, times(options.getMinSessions())).singleUse(any(TimestampBound.class)); - // Verify that MinSessions and WriteFraction are respected. - waitForExpectedSessionPool(options.getMinSessions(), options.getWriteSessionsFraction()); - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMinSessions()); - assertThat(pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo((int) Math.ceil(options.getMinSessions() * options.getWriteSessionsFraction())); - - pool.closeAsync(new SpannerImpl.ClosedException()).get(5L, TimeUnit.SECONDS); - } - - private void waitForExpectedSessionPool(int expectedSessions, float writeFraction) - throws InterruptedException { - Stopwatch watch = Stopwatch.createStarted(); - while ((pool.getNumberOfSessionsInPool() < expectedSessions - || pool.getNumberOfAvailableWritePreparedSessions() - < Math.ceil(expectedSessions * writeFraction)) - && watch.elapsed(TimeUnit.SECONDS) < 5) { - Thread.sleep(1L); + }); + // Wait until the background thread is actually waiting for a session. + latch.await(); + // Wait until the request has timed out. + int waitCount = 0; + while (pool.getNumWaiterTimeouts() == 0L && waitCount < 1000) { + Thread.sleep(5L); + waitCount++; } - } + // Return the checked out session to the pool so the async request will get a session and + // finish. + checkedOutSession.close(); + // Verify that the async request also succeeds. + fut.get(10L, TimeUnit.SECONDS); + executor.shutdown(); - @Test - public void blockAndTimeoutOnPoolExhaustion() throws Exception { - // Try to take a read or a read/write session. These requests should block. - for (Boolean write : new Boolean[] {true, false}) { - // Create a session pool with max 1 session and a low timeout for waiting for a session. - options = - SessionPoolOptions.newBuilder() - .setMinSessions(minSessions) - .setMaxSessions(1) - .setInitialWaitForSessionTimeoutMillis(20L) - .build(); - setupMockSessionCreation(); - pool = createPool(); - // Take the only session that can be in the pool. - PooledSessionFuture checkedOutSession = pool.getReadSession(); - checkedOutSession.get(); - final Boolean finWrite = write; - ExecutorService executor = Executors.newFixedThreadPool(1); - final CountDownLatch latch = new CountDownLatch(1); - // Then try asynchronously to take another session. This attempt should time out. - Future fut = - executor.submit( - new Callable() { - @Override - public Void call() { - PooledSessionFuture session; - latch.countDown(); - if (finWrite) { - session = pool.getReadWriteSession(); - } else { - session = pool.getReadSession(); - } - session.close(); - return null; - } - }); - // Wait until the background thread is actually waiting for a session. - latch.await(); - // Wait until the request has timed out. - int waitCount = 0; - while (pool.getNumWaiterTimeouts() == 0L && waitCount < 1000) { - Thread.sleep(5L); - waitCount++; - } - // Return the checked out session to the pool so the async request will get a session and - // finish. - checkedOutSession.close(); - // Verify that the async request also succeeds. - fut.get(10L, TimeUnit.SECONDS); - executor.shutdown(); - - // Verify that the session was returned to the pool and that we can get it again. - Session session = pool.getReadSession(); - assertThat(session).isNotNull(); - session.close(); - assertThat(pool.getNumWaiterTimeouts()).isAtLeast(1L); - } + // Verify that the session was returned to the pool and that we can get it again. + Session session = pool.getSession(); + assertThat(session).isNotNull(); + session.close(); + assertThat(pool.getNumWaiterTimeouts()).isAtLeast(1L); } @Test @@ -1247,7 +790,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - ReadContext context = pool.getReadSession().singleUse(); + ReadContext context = pool.getSession().singleUse(); ResultSet resultSet = context.executeQuery(statement); assertThat(resultSet.next()).isTrue(); } @@ -1303,7 +846,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - ReadOnlyTransaction transaction = pool.getReadSession().readOnlyTransaction(); + ReadOnlyTransaction transaction = pool.getSession().readOnlyTransaction(); ResultSet resultSet = transaction.executeQuery(statement); assertThat(resultSet.next()).isTrue(); } @@ -1327,248 +870,171 @@ public void testSessionNotFoundReadWriteTransaction() { for (ReadWriteTransactionTestStatementType statementType : ReadWriteTransactionTestStatementType.values()) { final ReadWriteTransactionTestStatementType executeStatementType = statementType; - for (boolean prepared : new boolean[] {true, false}) { - final boolean hasPreparedTransaction = prepared; - SpannerRpc.StreamingCall closedStreamingCall = mock(SpannerRpc.StreamingCall.class); - doThrow(sessionNotFound).when(closedStreamingCall).request(Mockito.anyInt()); - SpannerRpc rpc = mock(SpannerRpc.class); - when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) - .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); - when(rpc.executeQuery( - any(ExecuteSqlRequest.class), any(ResultStreamConsumer.class), any(Map.class))) - .thenReturn(closedStreamingCall); - when(rpc.executeQuery(any(ExecuteSqlRequest.class), any(Map.class))) - .thenThrow(sessionNotFound); - when(rpc.executeBatchDml(any(ExecuteBatchDmlRequest.class), any(Map.class))) - .thenThrow(sessionNotFound); - when(rpc.commitAsync(any(CommitRequest.class), any(Map.class))) - .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); - doThrow(sessionNotFound).when(rpc).rollback(any(RollbackRequest.class), any(Map.class)); - final SessionImpl closedSession = mock(SessionImpl.class); - when(closedSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); - ByteString preparedTransactionId = - hasPreparedTransaction ? ByteString.copyFromUtf8("test-txn") : null; - final TransactionContextImpl closedTransactionContext = - TransactionContextImpl.newBuilder() - .setSession(closedSession) - .setTransactionId(preparedTransactionId) - .setRpc(rpc) - .build(); - when(closedSession.asyncClose()) - .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); - when(closedSession.newTransaction()).thenReturn(closedTransactionContext); - when(closedSession.beginTransactionAsync()).thenThrow(sessionNotFound); - TransactionRunnerImpl closedTransactionRunner = - new TransactionRunnerImpl(closedSession, rpc, 10); - closedTransactionRunner.setSpan(mock(Span.class)); - when(closedSession.readWriteTransaction()).thenReturn(closedTransactionRunner); - - final SessionImpl openSession = mock(SessionImpl.class); - when(openSession.asyncClose()) - .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); - when(openSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); - final TransactionContextImpl openTransactionContext = mock(TransactionContextImpl.class); - when(openSession.newTransaction()).thenReturn(openTransactionContext); - when(openSession.beginTransactionAsync()) - .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn"))); - TransactionRunnerImpl openTransactionRunner = - new TransactionRunnerImpl(openSession, mock(SpannerRpc.class), 10); - openTransactionRunner.setSpan(mock(Span.class)); - when(openSession.readWriteTransaction()).thenReturn(openTransactionRunner); - - ResultSet openResultSet = mock(ResultSet.class); - when(openResultSet.next()).thenReturn(true, false); - ResultSet planResultSet = mock(ResultSet.class); - when(planResultSet.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); - when(openTransactionContext.executeQuery(queryStatement)).thenReturn(openResultSet); - when(openTransactionContext.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN)) - .thenReturn(planResultSet); - when(openTransactionContext.executeUpdate(updateStatement)).thenReturn(1L); - when(openTransactionContext.batchUpdate(Arrays.asList(updateStatement, updateStatement))) - .thenReturn(new long[] {1L, 1L}); - SpannerImpl spanner = mock(SpannerImpl.class); - SessionClient sessionClient = mock(SessionClient.class); - when(spanner.getSessionClient(db)).thenReturn(sessionClient); - - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(closedSession); - } - }); - return null; - } - }) - .doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(openSession); - } - }); - return null; + SpannerRpc.StreamingCall closedStreamingCall = mock(SpannerRpc.StreamingCall.class); + doThrow(sessionNotFound).when(closedStreamingCall).request(Mockito.anyInt()); + SpannerRpc rpc = mock(SpannerRpc.class); + when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(rpc.executeQuery( + any(ExecuteSqlRequest.class), any(ResultStreamConsumer.class), any(Map.class))) + .thenReturn(closedStreamingCall); + when(rpc.executeQuery(any(ExecuteSqlRequest.class), any(Map.class))) + .thenThrow(sessionNotFound); + when(rpc.executeBatchDml(any(ExecuteBatchDmlRequest.class), any(Map.class))) + .thenThrow(sessionNotFound); + when(rpc.commitAsync(any(CommitRequest.class), any(Map.class))) + .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); + when(rpc.rollbackAsync(any(RollbackRequest.class), any(Map.class))) + .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); + final SessionImpl closedSession = mock(SessionImpl.class); + when(closedSession.getName()) + .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); + final TransactionContextImpl closedTransactionContext = + TransactionContextImpl.newBuilder().setSession(closedSession).setRpc(rpc).build(); + when(closedSession.asyncClose()) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(closedSession.newTransaction()).thenReturn(closedTransactionContext); + when(closedSession.beginTransactionAsync()).thenThrow(sessionNotFound); + TransactionRunnerImpl closedTransactionRunner = + new TransactionRunnerImpl(closedSession, rpc, 10); + closedTransactionRunner.setSpan(mock(Span.class)); + when(closedSession.readWriteTransaction()).thenReturn(closedTransactionRunner); + + final SessionImpl openSession = mock(SessionImpl.class); + when(openSession.asyncClose()) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(openSession.getName()) + .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); + final TransactionContextImpl openTransactionContext = mock(TransactionContextImpl.class); + when(openSession.newTransaction()).thenReturn(openTransactionContext); + when(openSession.beginTransactionAsync()) + .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn"))); + TransactionRunnerImpl openTransactionRunner = + new TransactionRunnerImpl(openSession, mock(SpannerRpc.class), 10); + openTransactionRunner.setSpan(mock(Span.class)); + when(openSession.readWriteTransaction()).thenReturn(openTransactionRunner); + + ResultSet openResultSet = mock(ResultSet.class); + when(openResultSet.next()).thenReturn(true, false); + ResultSet planResultSet = mock(ResultSet.class); + when(planResultSet.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); + when(openTransactionContext.executeQuery(queryStatement)).thenReturn(openResultSet); + when(openTransactionContext.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN)) + .thenReturn(planResultSet); + when(openTransactionContext.executeUpdate(updateStatement)).thenReturn(1L); + when(openTransactionContext.batchUpdate(Arrays.asList(updateStatement, updateStatement))) + .thenReturn(new long[] {1L, 1L}); + SpannerImpl spanner = mock(SpannerImpl.class); + SessionClient sessionClient = mock(SessionClient.class); + when(spanner.getSessionClient(db)).thenReturn(sessionClient); + + doAnswer( + new Answer() { + @Override + public Void answer(final InvocationOnMock invocation) { + executor.submit( + new Runnable() { + @Override + public void run() { + SessionConsumerImpl consumer = + invocation.getArgumentAt(2, SessionConsumerImpl.class); + consumer.onSessionReady(closedSession); + } + }); + return null; + } + }) + .doAnswer( + new Answer() { + @Override + public Void answer(final InvocationOnMock invocation) { + executor.submit( + new Runnable() { + @Override + public void run() { + SessionConsumerImpl consumer = + invocation.getArgumentAt(2, SessionConsumerImpl.class); + consumer.onSessionReady(openSession); + } + }); + return null; + } + }) + .when(sessionClient) + .asyncBatchCreateSessions( + Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); + SessionPoolOptions options = + SessionPoolOptions.newBuilder() + .setMinSessions(0) // The pool should not auto-create any sessions + .setMaxSessions(2) + .setIncStep(1) + .setBlockIfPoolExhausted() + .build(); + SpannerOptions spannerOptions = mock(SpannerOptions.class); + when(spannerOptions.getSessionPoolOptions()).thenReturn(options); + when(spannerOptions.getNumChannels()).thenReturn(4); + when(spanner.getOptions()).thenReturn(spannerOptions); + SessionPool pool = + SessionPool.createPool(options, new TestExecutorFactory(), spanner.getSessionClient(db)); + try (PooledSessionFuture readWriteSession = pool.getSession()) { + TransactionRunner runner = readWriteSession.readWriteTransaction(); + try { + runner.run( + new TransactionCallable() { + private int callNumber = 0; + + @Override + public Integer run(TransactionContext transaction) { + callNumber++; + if (callNumber == 1) { + assertThat(transaction).isEqualTo(closedTransactionContext); + } else { + assertThat(transaction).isEqualTo(openTransactionContext); } - }) - .when(sessionClient) - .asyncBatchCreateSessions( - Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - SessionPoolOptions options = - SessionPoolOptions.newBuilder() - .setMinSessions(0) // The pool should not auto-create any sessions - .setMaxSessions(2) - .setIncStep(1) - .setBlockIfPoolExhausted() - .build(); - SpannerOptions spannerOptions = mock(SpannerOptions.class); - when(spannerOptions.getSessionPoolOptions()).thenReturn(options); - when(spannerOptions.getNumChannels()).thenReturn(4); - when(spanner.getOptions()).thenReturn(spannerOptions); - SessionPool pool = - SessionPool.createPool( - options, new TestExecutorFactory(), spanner.getSessionClient(db)); - try (PooledSessionFuture readWriteSession = pool.getReadWriteSession()) { - TransactionRunner runner = readWriteSession.readWriteTransaction(); - try { - runner.run( - new TransactionCallable() { - private int callNumber = 0; - - @Override - public Integer run(TransactionContext transaction) { - callNumber++; - if (hasPreparedTransaction) { - // If the session had a prepared read/write transaction, that transaction will - // be given to the runner in the first place and the SessionNotFoundException - // will occur on the first query / update statement. - if (callNumber == 1) { - assertThat(transaction).isEqualTo(closedTransactionContext); - } else { - assertThat(transaction).isEqualTo(openTransactionContext); - } - } else { - // If the session did not have a prepared read/write transaction, the library - // tried to create a new transaction before handing it to the transaction - // runner. The creation of the new transaction failed with a - // SessionNotFoundException, and the session was re-created before the run - // method was called. - assertThat(transaction).isEqualTo(openTransactionContext); - } - switch (executeStatementType) { - case QUERY: - ResultSet resultSet = transaction.executeQuery(queryStatement); - assertThat(resultSet.next()).isTrue(); - break; - case ANALYZE: - ResultSet planResultSet = - transaction.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN); - assertThat(planResultSet.next()).isFalse(); - assertThat(planResultSet.getStats()).isNotNull(); - break; - case UPDATE: - long updateCount = transaction.executeUpdate(updateStatement); - assertThat(updateCount).isEqualTo(1L); - break; - case BATCH_UPDATE: - long[] updateCounts = - transaction.batchUpdate( - Arrays.asList(updateStatement, updateStatement)); - assertThat(updateCounts).isEqualTo(new long[] {1L, 1L}); - break; - case WRITE: - transaction.buffer(Mutation.delete("FOO", Key.of(1L))); - break; - case EXCEPTION: - throw new RuntimeException("rollback at call " + callNumber); - default: - fail("Unknown statement type: " + executeStatementType); - } - return callNumber; + switch (executeStatementType) { + case QUERY: + ResultSet resultSet = transaction.executeQuery(queryStatement); + assertThat(resultSet.next()).isTrue(); + break; + case ANALYZE: + ResultSet planResultSet = + transaction.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN); + assertThat(planResultSet.next()).isFalse(); + assertThat(planResultSet.getStats()).isNotNull(); + break; + case UPDATE: + long updateCount = transaction.executeUpdate(updateStatement); + assertThat(updateCount).isEqualTo(1L); + break; + case BATCH_UPDATE: + long[] updateCounts = + transaction.batchUpdate(Arrays.asList(updateStatement, updateStatement)); + assertThat(updateCounts).isEqualTo(new long[] {1L, 1L}); + break; + case WRITE: + transaction.buffer(Mutation.delete("FOO", Key.of(1L))); + break; + case EXCEPTION: + throw new RuntimeException("rollback at call " + callNumber); + default: + fail("Unknown statement type: " + executeStatementType); } - }); - } catch (Exception e) { - // The rollback will also cause a SessionNotFoundException, but this is caught, logged - // and further ignored by the library, meaning that the session will not be re-created - // for retry. Hence rollback at call 1. - assertThat( - executeStatementType == ReadWriteTransactionTestStatementType.EXCEPTION - && e.getMessage().contains("rollback at call 1")) - .isTrue(); - } + return callNumber; + } + }); + } catch (Exception e) { + // The rollback will also cause a SessionNotFoundException, but this is caught, logged + // and further ignored by the library, meaning that the session will not be re-created + // for retry. Hence rollback at call 1. + assertThat(executeStatementType) + .isEqualTo(ReadWriteTransactionTestStatementType.EXCEPTION); + assertThat(e.getMessage()).contains("rollback at call 1"); } - pool.closeAsync(new SpannerImpl.ClosedException()); } + pool.closeAsync(new SpannerImpl.ClosedException()); } } - @Test - public void testSessionNotFoundOnPrepareTransaction() { - final SpannerException sessionNotFound = - SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName); - final SessionImpl closedSession = mock(SessionImpl.class); - when(closedSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); - when(closedSession.beginTransaction()).thenThrow(sessionNotFound); - doThrow(sessionNotFound).when(closedSession).prepareReadWriteTransaction(); - - final SessionImpl openSession = mock(SessionImpl.class); - when(openSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(closedSession); - } - }); - return null; - } - }) - .doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(openSession); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - FakeClock clock = new FakeClock(); - clock.currentTimeMillis = System.currentTimeMillis(); - pool = createPool(clock); - PooledSession session = pool.getReadWriteSession().get(); - assertThat(session.delegate).isEqualTo(openSession); - } - @Test public void testSessionNotFoundWrite() { SpannerException sessionNotFound = @@ -1745,8 +1211,8 @@ public void testSessionMetrics() throws Exception { setupMockSessionCreation(); pool = createPool(clock, metricRegistry, labelValues); - PooledSessionFuture session1 = pool.getReadSession(); - PooledSessionFuture session2 = pool.getReadSession(); + PooledSessionFuture session1 = pool.getSession(); + PooledSessionFuture session2 = pool.getSession(); session1.get(); session2.get(); @@ -1824,7 +1290,7 @@ public void testSessionMetrics() throws Exception { @Override public Void call() { latch.countDown(); - Session session = pool.getReadSession(); + Session session = pool.getSession(); session.close(); return null; } @@ -1882,7 +1348,7 @@ private void getSessionAsync(final CountDownLatch latch, final AtomicBoolean fai new Runnable() { @Override public void run() { - try (PooledSessionFuture future = pool.getReadSession()) { + try (PooledSessionFuture future = pool.getSession()) { PooledSession session = future.get(); failed.compareAndSet(false, session == null); Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS); @@ -1895,23 +1361,4 @@ public void run() { }) .start(); } - - private void getReadWriteSessionAsync(final CountDownLatch latch, final AtomicBoolean failed) { - new Thread( - new Runnable() { - @Override - public void run() { - try (PooledSessionFuture future = pool.getReadWriteSession()) { - PooledSession session = future.get(); - failed.compareAndSet(false, session == null); - Uninterruptibles.sleepUninterruptibly(2, TimeUnit.MILLISECONDS); - } catch (SpannerException e) { - failed.compareAndSet(false, true); - } finally { - latch.countDown(); - } - } - }) - .start(); - } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java index 7dcc9b65e1b..75552c52e1a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java @@ -96,22 +96,6 @@ public class SpanTest { private static final SimulatedExecutionTime ONE_SECOND = SimulatedExecutionTime.ofMinimumAndRandomTime(1000, 0); - private static final Statement SELECT1AND2 = - Statement.of("SELECT 1 AS COL1 UNION ALL SELECT 2 AS COL1"); - private static final ResultSetMetadata SELECT1AND2_METADATA = - ResultSetMetadata.newBuilder() - .setRowType( - StructType.newBuilder() - .addFields( - Field.newBuilder() - .setName("COL1") - .setType( - com.google.spanner.v1.Type.newBuilder() - .setCode(TypeCode.INT64) - .build()) - .build()) - .build()) - .build(); private static final StatusRuntimeException FAILED_PRECONDITION = io.grpc.Status.FAILED_PRECONDITION .withDescription("Non-retryable test exception.") @@ -162,11 +146,7 @@ public void setUp() throws Exception { .setProjectId(TEST_PROJECT) .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()) - .setSessionPoolOption( - SessionPoolOptions.newBuilder() - .setMinSessions(0) - .setWriteSessionsFraction(0.0f) - .build()); + .setSessionPoolOption(SessionPoolOptions.newBuilder().setMinSessions(0).build()); spanner = builder.build().getService(); @@ -227,7 +207,7 @@ public void tearDown() { @Test public void singleUseNonRetryableErrorOnNext() { - try (ResultSet rs = client.singleUse().executeQuery(SELECT1AND2)) { + try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) { mockSpanner.addException(FAILED_PRECONDITION); while (rs.next()) { // Just consume the result set. @@ -241,7 +221,7 @@ public void singleUseNonRetryableErrorOnNext() { @Test public void singleUseExecuteStreamingSqlTimeout() { - try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1AND2)) { + try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1)) { mockSpanner.setExecuteStreamingSqlExecutionTime(ONE_SECOND); while (rs.next()) { // Just consume the result set. @@ -302,7 +282,6 @@ public Void run(TransactionContext transaction) { assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); assertThat(spans).containsEntry("SessionPool.WaitForSession", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); - assertThat(spans).containsEntry("CloudSpannerOperation.BeginTransaction", true); assertThat(spans).containsEntry("CloudSpannerOperation.Commit", true); } @@ -324,11 +303,10 @@ public Void run(TransactionContext transaction) { } Map spans = failOnOverkillTraceComponent.getSpans(); - assertThat(spans.size()).isEqualTo(5); + assertThat(spans.size()).isEqualTo(4); assertThat(spans).containsEntry("CloudSpanner.ReadWriteTransaction", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); assertThat(spans).containsEntry("SessionPool.WaitForSession", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); - assertThat(spans).containsEntry("CloudSpannerOperation.BeginTransaction", true); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java index b98702f87c0..cda4cf5f8f7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java @@ -360,7 +360,7 @@ public void readWriteTransactionStatementAborted() { @Override public Long run(TransactionContext transaction) { if (attempts.getAndIncrement() == 0) { - mockSpanner.abortTransaction(transaction); + mockSpanner.abortNextStatement(); } return transaction.executeUpdate(UPDATE_STATEMENT); } @@ -418,7 +418,7 @@ public Long run(TransactionContext transaction) { @SuppressWarnings("resource") @Test public void transactionManagerTimeout() { - mockSpanner.setBeginTransactionExecutionTime(ONE_SECOND); + mockSpanner.setExecuteSqlExecutionTime(ONE_SECOND); try (TransactionManager txManager = clientWithTimeout.transactionManager()) { TransactionContext tx = txManager.begin(); while (true) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java index dec674bd6ca..0291e678687 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java @@ -194,7 +194,7 @@ public void testTransactionManagerAbortOnCommit() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } manager.commit(); break; @@ -219,7 +219,7 @@ public void testTransactionManagerAbortOnUpdate() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } long updateCount = txn.executeUpdate(UPDATE_STATEMENT); assertThat(updateCount, is(equalTo(UPDATE_COUNT))); @@ -246,7 +246,7 @@ public void testTransactionManagerAbortOnBatchUpdate() throws InterruptedExcepti attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } long[] updateCounts = txn.batchUpdate(Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); assertThat(updateCounts, is(equalTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}))); @@ -301,7 +301,7 @@ public void testTransactionManagerAbortOnSelect() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } try (ResultSet rs = txn.executeQuery(SELECT1AND2)) { int rows = 0; @@ -333,7 +333,7 @@ public void testTransactionManagerAbortOnRead() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } try (ResultSet rs = txn.read("FOO", KeySet.all(), Arrays.asList("BAR"))) { int rows = 0; @@ -365,7 +365,7 @@ public void testTransactionManagerAbortOnReadUsingIndex() throws InterruptedExce attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } try (ResultSet rs = txn.readUsingIndex("FOO", "INDEX", KeySet.all(), Arrays.asList("BAR"))) { @@ -398,7 +398,7 @@ public void testTransactionManagerAbortOnReadRow() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } Struct row = txn.readRow("FOO", Key.of(), Arrays.asList("BAR")); assertThat(row.getLong(0), is(equalTo(1L))); @@ -425,7 +425,7 @@ public void testTransactionManagerAbortOnReadRowUsingIndex() throws InterruptedE attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } Struct row = txn.readRowUsingIndex("FOO", "INDEX", Key.of(), Arrays.asList("BAR")); assertThat(row.getLong(0), is(equalTo(1L))); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 38aa66516ea..149002531ae 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -38,6 +38,10 @@ import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.CommitResponse; +import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.Transaction; import io.opencensus.trace.Span; import java.util.Arrays; @@ -46,6 +50,7 @@ import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -284,4 +289,112 @@ public ApiFuture answer(InvocationOnMock invocation) .beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); } } + + @SuppressWarnings({"unchecked", "resource"}) + @Test + public void inlineBegin() { + SpannerOptions options = mock(SpannerOptions.class); + when(options.getNumChannels()).thenReturn(4); + GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class); + when(transportOptions.getExecutorFactory()).thenReturn(new TestExecutorFactory()); + when(options.getTransportOptions()).thenReturn(transportOptions); + SessionPoolOptions sessionPoolOptions = + SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); + when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions); + when(options.getSessionLabels()).thenReturn(Collections.emptyMap()); + when(options.getDefaultQueryOptions(Mockito.any(DatabaseId.class))) + .thenReturn(QueryOptions.getDefaultInstance()); + SpannerRpc rpc = mock(SpannerRpc.class); + when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(rpc.batchCreateSessions( + Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + .thenAnswer( + new Answer>() { + @Override + public List answer(InvocationOnMock invocation) + throws Throwable { + return Arrays.asList( + com.google.spanner.v1.Session.newBuilder() + .setName((String) invocation.getArguments()[0] + "/sessions/1") + .setCreateTime( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(System.currentTimeMillis() * 1000)) + .build()); + } + }); + when(rpc.beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer>() { + @Override + public ApiFuture answer(InvocationOnMock invocation) throws Throwable { + return ApiFutures.immediateFuture( + Transaction.newBuilder() + .setId(ByteString.copyFromUtf8(UUID.randomUUID().toString())) + .build()); + } + }); + final AtomicInteger transactionsStarted = new AtomicInteger(); + when(rpc.executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer() { + @Override + public com.google.spanner.v1.ResultSet answer(InvocationOnMock invocation) + throws Throwable { + com.google.spanner.v1.ResultSet.Builder builder = + com.google.spanner.v1.ResultSet.newBuilder() + .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build()); + ExecuteSqlRequest request = invocation.getArgumentAt(0, ExecuteSqlRequest.class); + if (request.getTransaction() != null && request.getTransaction().hasBegin()) { + transactionsStarted.incrementAndGet(); + builder.setMetadata( + ResultSetMetadata.newBuilder() + .setTransaction( + Transaction.newBuilder() + .setId(ByteString.copyFromUtf8("test-tx")) + .build()) + .build()); + } + return builder.build(); + } + }); + when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer>() { + @Override + public ApiFuture answer(InvocationOnMock invocation) + throws Throwable { + return ApiFutures.immediateFuture( + CommitResponse.newBuilder() + .setCommitTimestamp( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(System.currentTimeMillis() * 1000)) + .build()); + } + }); + DatabaseId db = DatabaseId.of("test", "test", "test"); + try (SpannerImpl spanner = new SpannerImpl(rpc, options)) { + DatabaseClient client = spanner.getDatabaseClient(db); + try (TransactionManager mgr = client.transactionManager()) { + TransactionContext tx = mgr.begin(); + while (true) { + try { + tx.executeUpdate(Statement.of("UPDATE FOO SET BAR=1")); + tx.executeUpdate(Statement.of("UPDATE FOO SET BAZ=2")); + mgr.commit(); + break; + } catch (AbortedException e) { + tx = mgr.resetForRetry(); + } + } + } + // BeginTransaction should not be called, as we are inlining it with the ExecuteSql request. + verify(rpc, Mockito.never()) + .beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + // We should have 2 ExecuteSql requests. + verify(rpc, times(2)).executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap()); + // But only 1 with a BeginTransaction. + assertThat(transactionsStarted.get()).isEqualTo(1); + } + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java index d61c89300fa..71a34950bbd 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -44,8 +45,12 @@ import com.google.spanner.v1.CommitResponse; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteBatchDmlResponse; +import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import com.google.spanner.v1.ResultSet; +import com.google.spanner.v1.ResultSetMetadata; import com.google.spanner.v1.ResultSetStats; +import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.Transaction; import io.grpc.Metadata; import io.grpc.Status; @@ -90,12 +95,34 @@ public void release(ScheduledExecutorService exec) { @Mock private TransactionRunnerImpl.TransactionContextImpl txn; private TransactionRunnerImpl transactionRunner; private boolean firstRun; + private boolean usedInlinedBegin; @Before public void setUp() { MockitoAnnotations.initMocks(this); firstRun = true; when(session.newTransaction()).thenReturn(txn); + when(rpc.executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer() { + @Override + public ResultSet answer(InvocationOnMock invocation) throws Throwable { + ResultSet.Builder builder = + ResultSet.newBuilder() + .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build()); + ExecuteSqlRequest request = invocation.getArgumentAt(0, ExecuteSqlRequest.class); + if (request.getTransaction().hasBegin() + && request.getTransaction().getBegin().hasReadWrite()) { + builder.setMetadata( + ResultSetMetadata.newBuilder() + .setTransaction( + Transaction.newBuilder().setId(ByteString.copyFromUtf8("test"))) + .build()); + usedInlinedBegin = true; + } + return builder.build(); + } + }); transactionRunner = new TransactionRunnerImpl(session, rpc, 1); when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) .thenReturn( @@ -103,6 +130,8 @@ public void setUp() { CommitResponse.newBuilder() .setCommitTimestamp(Timestamp.getDefaultInstance()) .build())); + when(rpc.rollbackAsync(Mockito.any(RollbackRequest.class), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); transactionRunner.setSpan(mock(Span.class)); } @@ -188,7 +217,7 @@ public Void run(TransactionContext transaction) { } }); assertThat(numCalls.get()).isEqualTo(1); - verify(txn).ensureTxn(); + verify(txn, never()).ensureTxn(); verify(txn).commit(); } @@ -196,7 +225,7 @@ public Void run(TransactionContext transaction) { public void runAbort() { when(txn.isAborted()).thenReturn(true); runTransaction(abortedWithRetryInfo()); - verify(txn, times(2)).ensureTxn(); + verify(txn).ensureTxn(); } @Test @@ -214,7 +243,8 @@ public Void run(TransactionContext transaction) { } }); assertThat(numCalls.get()).isEqualTo(2); - verify(txn, times(2)).ensureTxn(); + // ensureTxn() is only called during retry. + verify(txn).ensureTxn(); } @Test @@ -238,7 +268,7 @@ public Void run(TransactionContext transaction) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); } assertThat(numCalls.get()).isEqualTo(1); - verify(txn, times(1)).ensureTxn(); + verify(txn, never()).ensureTxn(); verify(txn, times(1)).commit(); } @@ -274,6 +304,42 @@ public void batchDmlFailedPrecondition() { } } + @SuppressWarnings("unchecked") + @Test + public void inlineBegin() { + SpannerImpl spanner = mock(SpannerImpl.class); + when(spanner.getRpc()).thenReturn(rpc); + when(spanner.getDefaultQueryOptions(Mockito.any(DatabaseId.class))) + .thenReturn(QueryOptions.getDefaultInstance()); + SessionImpl session = + new SessionImpl( + spanner, "projects/p/instances/i/databases/d/sessions/s", Collections.EMPTY_MAP) { + @Override + public void prepareReadWriteTransaction() { + // Using a prepared transaction is not allowed when the beginTransaction should be + // inlined with the first statement. + throw new IllegalStateException(); + } + }; + session.setCurrentSpan(mock(Span.class)); + TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10); + runner.setSpan(mock(Span.class)); + assertThat(usedInlinedBegin).isFalse(); + runner.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2")); + return null; + } + }); + verify(rpc, Mockito.never()) + .beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + verify(rpc, Mockito.never()) + .beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + assertThat(usedInlinedBegin).isTrue(); + } + @SuppressWarnings("unchecked") private long[] batchDmlException(int status) { Preconditions.checkArgument(status != Code.OK_VALUE); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java index 1e00015cdfb..22dc4c5c459 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java @@ -245,21 +245,21 @@ public void testTransactionManager() throws InterruptedException { for (int run = 0; run < 2; run++) { try (TransactionManager manager = client.transactionManager()) { TransactionContext txn = manager.begin(); - while (true) { - for (int i = 0; i < 2; i++) { - try (ResultSet rs = txn.executeQuery(Statement.of("SELECT 1"))) { - assertThat(rs.next()).isTrue(); - assertThat(rs.getLong(0)).isEqualTo(1L); - assertThat(rs.next()).isFalse(); + try { + while (true) { + for (int i = 0; i < 2; i++) { + try (ResultSet rs = txn.executeQuery(Statement.of("SELECT 1"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } } - } - try { manager.commit(); break; - } catch (AbortedException e) { - Thread.sleep(e.getRetryDelayInMillis() / 1000); - txn = manager.resetForRetry(); } + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + txn = manager.resetForRetry(); } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java index aabf93b3a6c..915efa604e7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java @@ -80,7 +80,8 @@ public static void setUpDatabase() { } @Before - public void increaseTestId() { + public void increaseTestIdAndDeleteTestData() { + client.writeAtLeastOnce(Arrays.asList(Mutation.delete("T", KeySet.all()))); id++; } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java index ab9acd90156..7b4f340f0b2 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java @@ -36,6 +36,7 @@ import com.google.cloud.spanner.Key; import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TransactionContext; @@ -47,6 +48,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -59,7 +61,8 @@ @RunWith(Parameterized.class) public class ITTransactionManagerAsyncTest { - @Parameter public Executor executor; + @Parameter(0) + public Executor executor; @Parameters(name = "executor = {0}") public static Collection data() { @@ -67,13 +70,14 @@ public static Collection data() { new Object[][] { {MoreExecutors.directExecutor()}, {Executors.newSingleThreadExecutor()}, - {Executors.newFixedThreadPool(4)} + {Executors.newFixedThreadPool(4)}, }); } @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - private static DatabaseClient client; + private Spanner spanner; + private DatabaseClient client; @BeforeClass public static void setUpDatabase() { @@ -85,14 +89,20 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " BoolValue BOOL," + ") PRIMARY KEY (K)"); - client = env.getTestHelper().getDatabaseClient(db); } @Before public void clearTable() { + spanner = env.getTestHelper().getClient(); + client = spanner.getDatabaseClient(db.getId()); client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); } + @After + public void closeSpanner() { + spanner.close(); + } + @Test public void testSimpleInsert() throws ExecutionException, InterruptedException { try (AsyncTransactionManager manager = client.transactionManagerAsync()) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java index 06087b9afec..4d65af67ed0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java @@ -27,6 +27,7 @@ import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.SpannerException; @@ -34,7 +35,9 @@ import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionManager; import com.google.cloud.spanner.TransactionManager.TransactionState; +import com.google.common.collect.ImmutableList; import java.util.Arrays; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -63,6 +66,11 @@ public static void setUpDatabase() { client = env.getTestHelper().getDatabaseClient(db); } + @Before + public void deleteTestData() { + client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); + } + @SuppressWarnings("resource") @Test public void simpleInsert() throws InterruptedException { @@ -201,6 +209,7 @@ public void abortAndRetry() throws InterruptedException { Struct row = client.singleUse().readRow("T", Key.of("Key3"), Arrays.asList("K", "BoolValue")); assertThat(row.getString(0)).isEqualTo("Key3"); assertThat(row.getBoolean(1)).isTrue(); + manager2.close(); } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index ed3450893d9..503f0ddf908 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -17,7 +17,6 @@ package com.google.cloud.spanner.it; import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; -import static com.google.cloud.spanner.TransactionRunner.TransactionCallable; import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -37,6 +36,7 @@ import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.PartitionOptions; import com.google.cloud.spanner.ReadContext; +import com.google.cloud.spanner.ReadOnlyTransaction; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; @@ -44,6 +44,9 @@ import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionRunner; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.cloud.spanner.testing.EmulatorSpannerHelper; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.Uninterruptibles; @@ -53,6 +56,7 @@ import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -82,6 +86,11 @@ public static void setUpDatabase() { client = env.getTestHelper().getDatabaseClient(db); } + @Before + public void removeTestData() { + client.writeAtLeastOnce(Arrays.asList(Mutation.delete("T", KeySet.all()))); + } + private static String uniqueKey() { return "k" + seq++; } @@ -422,7 +431,9 @@ public void nestedReadOnlyTxnThrows() { new TransactionCallable() { @Override public Void run(TransactionContext transaction) throws SpannerException { - client.readOnlyTransaction().getReadTimestamp(); + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + tx.getReadTimestamp(); + } return null; } @@ -506,4 +517,117 @@ public Void run(TransactionContext transaction) throws SpannerException { } }); } + + @Test + public void testTxWithCaughtError() { + assumeFalse( + "Emulator does not recover from an error within a transaction", + EmulatorSpannerHelper.isUsingEmulator()); + + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + transaction.executeUpdate(Statement.of("UPDATE T SET V=2 WHERE")); + fail("missing expected exception"); + } catch (SpannerException e) { + if (e.getErrorCode() == ErrorCode.ABORTED) { + // Aborted -> Let the transaction be retried + throw e; + } + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); + } + }); + assertThat(updateCount).isEqualTo(1L); + } + + @Test + public void testTxWithConstraintError() { + assumeFalse( + "Emulator does not recover from an error within a transaction", + EmulatorSpannerHelper.isUsingEmulator()); + + // First insert a single row. + client.writeAtLeastOnce( + ImmutableList.of( + Mutation.newInsertOrUpdateBuilder("T").set("K").to("One").set("V").to(1L).build())); + + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + // Try to insert a duplicate row. This statement will fail. When the statement + // is executed against an already existing transaction (i.e. + // inlineBegin=false), the entire transaction will remain invalid and cannot + // be committed. When it is executed as the first statement of a transaction + // that also tries to start a transaction, then no transaction will be started + // and the next statement will start the transaction. This will cause the + // transaction to succeed. + transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); + fail("missing expected exception"); + } catch (SpannerException e) { + if (e.getErrorCode() == ErrorCode.ABORTED) { + // Aborted -> Let the transaction be retried + throw e; + } + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); + } + return transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('Two', 2)")); + } + }); + fail("missing expected ALREADY_EXISTS error"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); + } + } + + @Test + public void testTxWithUncaughtError() { + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(Statement.of("UPDATE T SET V=2 WHERE")); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } + + @Test + public void testTxWithUncaughtErrorAfterSuccessfulBegin() { + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); + return transaction.executeUpdate(Statement.of("UPDATE T SET V=2 WHERE")); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } } From e1843ef38580fecb1f017330f3fa1447028607c7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 23 Oct 2020 02:40:25 +0200 Subject: [PATCH 76/79] deps: update opencensus.version to v0.28.2 (#538) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 54e0cc89513..f72ee2872b7 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.28.1 + 0.28.2 1.2.4 2.0.5 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index e8cefdd6f60..c81e2c611d4 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.28.1 + 0.28.2 1.2.4 2.0.5 diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index c3465a18e27..205232a160a 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 0.28.1 + 0.28.2 From ce3bed6f5359224c37502331a9f776e29632d3a5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 23 Oct 2020 02:41:26 +0200 Subject: [PATCH 77/79] deps: update dependency com.google.cloud:google-cloud-monitoring to v2.0.6 (#540) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index f72ee2872b7..a33d00a0098 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.28.2 1.2.4 - 2.0.5 + 2.0.6 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index c81e2c611d4..a0f42a0dbf0 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.28.2 1.2.4 - 2.0.5 + 2.0.6 From eddd6ad4e5093ee21290b85f15fa432d071bae59 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 23 Oct 2020 05:19:15 +0200 Subject: [PATCH 78/79] deps: update dependency com.google.cloud:google-cloud-trace to v1.2.5 (#539) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index a33d00a0098..dceea3d1788 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.28.2 - 1.2.4 + 1.2.5 2.0.6 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index a0f42a0dbf0..c6925e3a3c5 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.28.2 - 1.2.4 + 1.2.5 2.0.6 From 0646853232c6307f88585f67d3e5997ef0a384b5 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 10:00:16 +1100 Subject: [PATCH 79/79] chore: release 3.0.0 (#502) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 43 +++++++++++++++++++ google-cloud-spanner-bom/pom.xml | 18 ++++---- google-cloud-spanner/pom.xml | 4 +- .../pom.xml | 4 +- .../pom.xml | 4 +- grpc-google-cloud-spanner-v1/pom.xml | 4 +- pom.xml | 16 +++---- .../pom.xml | 4 +- .../pom.xml | 4 +- proto-google-cloud-spanner-v1/pom.xml | 4 +- samples/snapshot/pom.xml | 2 +- versions.txt | 14 +++--- 12 files changed, 82 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e23ca8a48f..197f825b36b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## [3.0.0](https://www.github.com/googleapis/java-spanner/compare/v2.0.2...v3.0.0) (2020-10-23) + + +### ⚠ BREAKING CHANGES + +* initialize should be protected (#536) +* async connection API (#392) + +### Features + +* adds options to the write operations ([#531](https://www.github.com/googleapis/java-spanner/issues/531)) ([659719d](https://www.github.com/googleapis/java-spanner/commit/659719deb5a18a87859bc174f5bde1e1147834d8)) +* async connection API ([#392](https://www.github.com/googleapis/java-spanner/issues/392)) ([3dd0675](https://www.github.com/googleapis/java-spanner/commit/3dd0675d2d7882d40a6af1e12fda3b4617019870)), closes [#378](https://www.github.com/googleapis/java-spanner/issues/378) +* inline begin transaction ([#325](https://www.github.com/googleapis/java-spanner/issues/325)) ([d08d3de](https://www.github.com/googleapis/java-spanner/commit/d08d3debb6457548bb6b04335b7a2d2227369211)), closes [#515](https://www.github.com/googleapis/java-spanner/issues/515) + + +### Bug Fixes + +* AsyncTransactionManager did not propagate statement errors ([#516](https://www.github.com/googleapis/java-spanner/issues/516)) ([4b8b845](https://www.github.com/googleapis/java-spanner/commit/4b8b8452589d63f6768b971a880a19bde80a9671)), closes [#514](https://www.github.com/googleapis/java-spanner/issues/514) +* AsyncTransactionManager should rollback on close ([#505](https://www.github.com/googleapis/java-spanner/issues/505)) ([c580df8](https://www.github.com/googleapis/java-spanner/commit/c580df8e1175bde293890c2a68e8816951c068d3)), closes [#504](https://www.github.com/googleapis/java-spanner/issues/504) +* close executor when closing pool ([#501](https://www.github.com/googleapis/java-spanner/issues/501)) ([2086746](https://www.github.com/googleapis/java-spanner/commit/208674632b20b37f51b828c1c4cc76c91154952b)) +* fixes javadocs for Key ([#532](https://www.github.com/googleapis/java-spanner/issues/532)) ([768c19d](https://www.github.com/googleapis/java-spanner/commit/768c19dc1b9985f7823ec1e4ca92491936062f3b)) +* fixes sample tests ([ed0665c](https://www.github.com/googleapis/java-spanner/commit/ed0665c71abbce57a28cb79531783145eccab1fb)) +* ignores failing backup operations ([2ad0b7f](https://www.github.com/googleapis/java-spanner/commit/2ad0b7fc6d1369795702484181ee11ecf59a1f8b)) +* increase visibility of #get() ([#486](https://www.github.com/googleapis/java-spanner/issues/486)) ([fa6d964](https://www.github.com/googleapis/java-spanner/commit/fa6d9641b7b2a5bb1d00de6b99b0f8bc157245d6)) +* initialize should be protected ([#536](https://www.github.com/googleapis/java-spanner/issues/536)) ([5c4c8c5](https://www.github.com/googleapis/java-spanner/commit/5c4c8c58674490ba524b678b409b8b19184af02f)) +* remove dependency on commons-lang ([#494](https://www.github.com/googleapis/java-spanner/issues/494)) ([c99294b](https://www.github.com/googleapis/java-spanner/commit/c99294beb43ce1bd67cc3d12e4104641efab6710)) + + +### Dependencies + +* update dependency com.google.cloud:google-cloud-monitoring to v2 ([#498](https://www.github.com/googleapis/java-spanner/issues/498)) ([3ab7348](https://www.github.com/googleapis/java-spanner/commit/3ab7348781e56384921d8287a5b5c0725dfed221)) +* update dependency com.google.cloud:google-cloud-monitoring to v2.0.5 ([#525](https://www.github.com/googleapis/java-spanner/issues/525)) ([fb874ec](https://www.github.com/googleapis/java-spanner/commit/fb874ec2e1738d569d585d30825a6e9d3de96c66)) +* update dependency com.google.cloud:google-cloud-monitoring to v2.0.6 ([#540](https://www.github.com/googleapis/java-spanner/issues/540)) ([ce3bed6](https://www.github.com/googleapis/java-spanner/commit/ce3bed6f5359224c37502331a9f776e29632d3a5)) +* update dependency com.google.cloud:google-cloud-shared-dependencies to v0.10.2 ([#500](https://www.github.com/googleapis/java-spanner/issues/500)) ([eb59929](https://www.github.com/googleapis/java-spanner/commit/eb5992949de326326a6bb02ec75b4a2a65a37b84)) +* update dependency com.google.cloud:google-cloud-trace to v1.2.3 ([#496](https://www.github.com/googleapis/java-spanner/issues/496)) ([0595a80](https://www.github.com/googleapis/java-spanner/commit/0595a80d5a6bb09e62ce1b6d101a3a039896c7af)) +* update dependency com.google.cloud:google-cloud-trace to v1.2.4 ([#526](https://www.github.com/googleapis/java-spanner/issues/526)) ([1020989](https://www.github.com/googleapis/java-spanner/commit/1020989e1ec1ad7f5185579da58d7a839167f05a)) +* update dependency com.google.cloud:google-cloud-trace to v1.2.5 ([#539](https://www.github.com/googleapis/java-spanner/issues/539)) ([eddd6ad](https://www.github.com/googleapis/java-spanner/commit/eddd6ad4e5093ee21290b85f15fa432d071bae59)) +* update dependency org.openjdk.jmh:jmh-core to v1.26 ([#506](https://www.github.com/googleapis/java-spanner/issues/506)) ([0f13c4c](https://www.github.com/googleapis/java-spanner/commit/0f13c4c5db37a736e391c002ed2456d78d04a090)) +* update dependency org.openjdk.jmh:jmh-generator-annprocess to v1.26 ([#507](https://www.github.com/googleapis/java-spanner/issues/507)) ([600f397](https://www.github.com/googleapis/java-spanner/commit/600f397a37f1808eb387fa3c31be0be5bb076c77)) +* update opencensus.version to v0.27.1 ([#497](https://www.github.com/googleapis/java-spanner/issues/497)) ([62fa39a](https://www.github.com/googleapis/java-spanner/commit/62fa39a2fbac6aa667073f16898e6861f0f5ec21)) +* update opencensus.version to v0.28.1 ([#533](https://www.github.com/googleapis/java-spanner/issues/533)) ([777f5fc](https://www.github.com/googleapis/java-spanner/commit/777f5fc486de7a54801c9f3f82adca561388ebfe)) +* update opencensus.version to v0.28.2 ([#538](https://www.github.com/googleapis/java-spanner/issues/538)) ([e1843ef](https://www.github.com/googleapis/java-spanner/commit/e1843ef38580fecb1f017330f3fa1447028607c7)) + ### [2.0.2](https://www.github.com/googleapis/java-spanner/compare/v2.0.1...v2.0.2) (2020-10-02) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 589a6606387..6ac4096f612 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 2.0.3-SNAPSHOT + 3.0.0 pom com.google.cloud @@ -64,43 +64,43 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.cloud google-cloud-spanner - 2.0.3-SNAPSHOT + 3.0.0 com.google.cloud google-cloud-spanner test-jar - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index c88858f91cf..dd25cc4282b 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 2.0.3-SNAPSHOT + 3.0.0 jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 2.0.3-SNAPSHOT + 3.0.0 google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index d6664483ba5..d01643b8c5b 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 2.0.3-SNAPSHOT + 3.0.0 grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index ebeb1bc6f0c..0e9f56798a2 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 2.0.3-SNAPSHOT + 3.0.0 grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 5ac2e93fbfa..11aed44c564 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 2.0.3-SNAPSHOT + 3.0.0 grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/pom.xml b/pom.xml index 78835421590..143510d62b3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 2.0.3-SNAPSHOT + 3.0.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -71,37 +71,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 2.0.3-SNAPSHOT + 3.0.0 com.google.cloud google-cloud-spanner - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index 6f72dbabfda..7646276dd3d 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 2.0.3-SNAPSHOT + 3.0.0 proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index c62a5297739..886bc032683 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 2.0.3-SNAPSHOT + 3.0.0 proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 670d307b11b..9877d4f86d8 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 2.0.3-SNAPSHOT + 3.0.0 proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index c6925e3a3c5..071d5830d3a 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 2.0.3-SNAPSHOT + 3.0.0 diff --git a/versions.txt b/versions.txt index e73ffd448ec..2ef2dd9f99d 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:2.0.2:2.0.3-SNAPSHOT -proto-google-cloud-spanner-v1:2.0.2:2.0.3-SNAPSHOT -proto-google-cloud-spanner-admin-database-v1:2.0.2:2.0.3-SNAPSHOT -grpc-google-cloud-spanner-v1:2.0.2:2.0.3-SNAPSHOT -grpc-google-cloud-spanner-admin-instance-v1:2.0.2:2.0.3-SNAPSHOT -grpc-google-cloud-spanner-admin-database-v1:2.0.2:2.0.3-SNAPSHOT -google-cloud-spanner:2.0.2:2.0.3-SNAPSHOT \ No newline at end of file +proto-google-cloud-spanner-admin-instance-v1:3.0.0:3.0.0 +proto-google-cloud-spanner-v1:3.0.0:3.0.0 +proto-google-cloud-spanner-admin-database-v1:3.0.0:3.0.0 +grpc-google-cloud-spanner-v1:3.0.0:3.0.0 +grpc-google-cloud-spanner-admin-instance-v1:3.0.0:3.0.0 +grpc-google-cloud-spanner-admin-database-v1:3.0.0:3.0.0 +google-cloud-spanner:3.0.0:3.0.0 \ No newline at end of file