parts = NAME_TEMPLATE.match(name);
+ Preconditions.checkArgument(
+ parts != null, "Name should conform to pattern %s: %s", NAME_TEMPLATE, name);
+ return of(parts.get("project"), parts.get("instance"), parts.get("backup"));
+ }
+
+ /**
+ * Creates a {@code BackupId} given project, instance and backup IDs. The backup id must conform
+ * to the regular expression [a-z][a-z0-9_\-]*[a-z0-9] and be between 2 and 60 characters in
+ * length.
+ */
+ public static BackupId of(String project, String instance, String backup) {
+ return new BackupId(new InstanceId(project, instance), backup);
+ }
+
+ /**
+ * Creates a {@code BackupId} given the instance identity and backup id. The backup id must
+ * conform to the regular expression [a-z][a-z0-9_\-]*[a-z0-9] and be between 2 and 60 characters
+ * in length.
+ */
+ public static BackupId of(InstanceId instanceId, String backup) {
+ return new BackupId(instanceId, backup);
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
new file mode 100644
index 0000000000..5968bcdc21
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
@@ -0,0 +1,196 @@
+/*
+ * 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.client.util.Preconditions;
+import com.google.cloud.Timestamp;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Represents a Cloud Spanner database backup. */
+public class BackupInfo {
+ public abstract static class Builder {
+ abstract Builder setState(State state);
+
+ abstract Builder setSize(long size);
+
+ abstract Builder setProto(com.google.spanner.admin.database.v1.Backup proto);
+
+ /**
+ * Required for creating a new backup.
+ *
+ * Sets the expiration time of the backup. The expiration time of the backup, with
+ * microseconds granularity that must be at least 6 hours and at most 366 days from the time the
+ * request is received. Once the expireTime has passed, Cloud Spanner will delete the backup and
+ * free the resources used by the backup.
+ */
+ public abstract Builder setExpireTime(Timestamp expireTime);
+
+ /**
+ * Required for creating a new backup.
+ *
+ *
Sets the source database to use for creating the backup.
+ */
+ public abstract Builder setDatabase(DatabaseId database);
+
+ /** Builds the backup from this builder. */
+ public abstract Backup build();
+ }
+
+ abstract static class BuilderImpl extends Builder {
+ protected final BackupId id;
+ private State state = State.UNSPECIFIED;
+ private Timestamp expireTime;
+ private DatabaseId database;
+ private long size;
+ private com.google.spanner.admin.database.v1.Backup proto;
+
+ BuilderImpl(BackupId id) {
+ this.id = Preconditions.checkNotNull(id);
+ }
+
+ BuilderImpl(BackupInfo other) {
+ this.id = other.id;
+ this.state = other.state;
+ this.expireTime = other.expireTime;
+ this.database = other.database;
+ this.size = other.size;
+ this.proto = other.proto;
+ }
+
+ @Override
+ Builder setState(State state) {
+ this.state = Preconditions.checkNotNull(state);
+ return this;
+ }
+
+ @Override
+ public Builder setExpireTime(Timestamp expireTime) {
+ this.expireTime = Preconditions.checkNotNull(expireTime);
+ return this;
+ }
+
+ @Override
+ public Builder setDatabase(DatabaseId database) {
+ Preconditions.checkArgument(
+ database.getInstanceId().equals(id.getInstanceId()),
+ "The instance of the source database must be equal to the instance of the backup.");
+ this.database = Preconditions.checkNotNull(database);
+ return this;
+ }
+
+ @Override
+ Builder setSize(long size) {
+ this.size = size;
+ return this;
+ }
+
+ @Override
+ Builder setProto(@Nullable com.google.spanner.admin.database.v1.Backup proto) {
+ this.proto = proto;
+ return this;
+ }
+ }
+
+ /** State of the backup. */
+ public enum State {
+ // Not specified.
+ UNSPECIFIED,
+ // The backup is still being created and is not ready to use.
+ CREATING,
+ // The backup is fully created and ready to use.
+ READY,
+ }
+
+ private final BackupId id;
+ private final State state;
+ private final Timestamp expireTime;
+ private final DatabaseId database;
+ private final long size;
+ private final com.google.spanner.admin.database.v1.Backup proto;
+
+ BackupInfo(BuilderImpl builder) {
+ this.id = builder.id;
+ this.state = builder.state;
+ this.size = builder.size;
+ this.expireTime = builder.expireTime;
+ this.database = builder.database;
+ this.proto = builder.proto;
+ }
+
+ /** Returns the backup id. */
+ public BackupId getId() {
+ return id;
+ }
+
+ /** Returns the id of the instance that the backup belongs to. */
+ public InstanceId getInstanceId() {
+ return id.getInstanceId();
+ }
+
+ /** Returns the state of the backup. */
+ public State getState() {
+ return state;
+ }
+
+ /** Returns the size of the backup in bytes. */
+ public long getSize() {
+ return size;
+ }
+
+ /** Returns the expire time of the backup. */
+ public Timestamp getExpireTime() {
+ return expireTime;
+ }
+
+ /** Returns the id of the database that was used to create the backup. */
+ public DatabaseId getDatabase() {
+ return database;
+ }
+
+ /** Returns the raw proto instance that was used to construct this {@link Backup}. */
+ public @Nullable com.google.spanner.admin.database.v1.Backup getProto() {
+ return proto;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ BackupInfo that = (BackupInfo) o;
+ return id.equals(that.id)
+ && state == that.state
+ && size == that.size
+ && Objects.equals(expireTime, that.expireTime)
+ && Objects.equals(database, that.database);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, state, size, expireTime, database);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Backup[%s, %s, %d, %s, %s]", id.getName(), state, size, expireTime, database);
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java
index 307cad1ed7..89ee6e875a 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java
@@ -19,7 +19,12 @@
import static com.google.common.base.Preconditions.checkArgument;
import com.google.api.gax.longrunning.OperationFuture;
+import com.google.api.gax.paging.Page;
import com.google.cloud.Policy;
+import com.google.cloud.Timestamp;
+import com.google.common.base.Preconditions;
+import com.google.longrunning.Operation;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
/**
@@ -27,7 +32,26 @@
* functionality over {@code DatabaseInfo}.
*/
public class Database extends DatabaseInfo {
+ public static class Builder extends DatabaseInfo.BuilderImpl {
+ private final DatabaseAdminClient dbClient;
+ Builder(DatabaseAdminClient dbClient, DatabaseId databaseId) {
+ super(databaseId);
+ this.dbClient = Preconditions.checkNotNull(dbClient);
+ }
+
+ private Builder(Database database) {
+ super(database);
+ this.dbClient = database.dbClient;
+ }
+
+ @Override
+ public Database build() {
+ return new Database(this);
+ }
+ }
+
+ private static final String FILTER_DB_OPERATIONS_TEMPLATE = "name:databases/%s";
private final DatabaseAdminClient dbClient;
public Database(DatabaseId id, State state, DatabaseAdminClient dbClient) {
@@ -35,6 +59,11 @@ public Database(DatabaseId id, State state, DatabaseAdminClient dbClient) {
this.dbClient = dbClient;
}
+ Database(Builder builder) {
+ super(builder);
+ this.dbClient = Preconditions.checkNotNull(builder.dbClient);
+ }
+
/** Fetches the database's current information. */
public Database reload() throws SpannerException {
return dbClient.getDatabase(instance(), database());
@@ -63,6 +92,36 @@ public void drop() throws SpannerException {
dbClient.dropDatabase(instance(), database());
}
+ /**
+ * Returns true
if a database with the id of this {@link Database} exists on Cloud
+ * Spanner.
+ */
+ public boolean exists() {
+ try {
+ dbClient.getDatabase(instance(), database());
+ } catch (SpannerException e) {
+ if (e.getErrorCode() == ErrorCode.NOT_FOUND) {
+ return false;
+ }
+ throw e;
+ }
+ return true;
+ }
+
+ /**
+ * Backs up this database to the location specified by the {@link Backup}. The given {@link
+ * Backup} must have an expire time. The backup must belong to the same instance as this database.
+ */
+ public OperationFuture backup(Backup backup) {
+ Preconditions.checkArgument(
+ backup.getExpireTime() != null, "The backup does not have an expire time.");
+ Preconditions.checkArgument(
+ backup.getInstanceId().equals(getId().getInstanceId()),
+ "The instance of the backup must be equal to the instance of this database.");
+ return dbClient.createBackup(
+ instance(), backup.getId().getBackup(), database(), backup.getExpireTime());
+ }
+
/**
* Returns the schema of a Cloud Spanner database as a list of formatted DDL statements. This
* method does not show pending schema updates.
@@ -71,6 +130,12 @@ public Iterable getDdl() throws SpannerException {
return dbClient.getDatabaseDdl(instance(), database());
}
+ /** Returns the long-running operations for this database. */
+ public Page listDatabaseOperations() {
+ return dbClient.listDatabaseOperations(
+ instance(), Options.filter(String.format(FILTER_DB_OPERATIONS_TEMPLATE, database())));
+ }
+
/** Returns the IAM {@link Policy} for this database. */
public Policy getIAMPolicy() {
return dbClient.getDatabaseIAMPolicy(instance(), database());
@@ -108,7 +173,12 @@ private String database() {
static Database fromProto(
com.google.spanner.admin.database.v1.Database proto, DatabaseAdminClient client) {
checkArgument(!proto.getName().isEmpty(), "Missing expected 'name' field");
- return new Database(DatabaseId.of(proto.getName()), fromProtoState(proto.getState()), client);
+ return new Database.Builder(client, DatabaseId.of(proto.getName()))
+ .setState(fromProtoState(proto.getState()))
+ .setCreateTime(Timestamp.fromProto(proto.getCreateTime()))
+ .setRestoreInfo(RestoreInfo.fromProtoOrNullIfDefaultInstance(proto.getRestoreInfo()))
+ .setProto(proto)
+ .build();
}
static DatabaseInfo.State fromProtoState(
@@ -120,6 +190,8 @@ static DatabaseInfo.State fromProtoState(
return DatabaseInfo.State.CREATING;
case READY:
return DatabaseInfo.State.READY;
+ case READY_OPTIMIZING:
+ return DatabaseInfo.State.READY_OPTIMIZING;
default:
throw new IllegalArgumentException("Unrecognized state " + protoState);
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
index a4f81ce11d..ed736b92ad 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
@@ -19,8 +19,12 @@
import com.google.api.gax.longrunning.OperationFuture;
import com.google.api.gax.paging.Page;
import com.google.cloud.Policy;
+import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Options.ListOption;
+import com.google.longrunning.Operation;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import java.util.List;
import javax.annotation.Nullable;
@@ -64,6 +68,75 @@ public interface DatabaseAdminClient {
OperationFuture createDatabase(
String instanceId, String databaseId, Iterable statements) throws SpannerException;
+ /** Returns a builder for a {@code Backup} object with the given id. */
+ Backup.Builder newBackupBuilder(BackupId id);
+
+ /**
+ * Creates a new backup from a database in a Cloud Spanner instance.
+ *
+ * Example to create a backup.
+ *
+ *
{@code
+ * String instance = my_instance_id;
+ * String backupId = my_backup_id;
+ * String databaseId = my_database_id;
+ * Timestamp expireTime = Timestamp.ofTimeMicroseconds(micros);
+ * OperationFuture op = dbAdminClient
+ * .createBackup(
+ * instanceId,
+ * backupId,
+ * databaseId,
+ * expireTime);
+ * Backup backup = op.get();
+ * }
+ *
+ * @param instanceId the id of the instance where the database to backup is located and where the
+ * backup will be created.
+ * @param backupId the id of the backup which will be created. It must conform to the regular
+ * expression [a-z][a-z0-9_\-]*[a-z0-9] and be between 2 and 60 characters in length.
+ * @param databaseId the id of the database to backup.
+ * @param expireTime the time that the backup will automatically expire.
+ */
+ OperationFuture createBackup(
+ String sourceInstanceId, String backupId, String databaseId, Timestamp expireTime)
+ throws SpannerException;
+
+ /**
+ * Restore a database from a backup. The database that is restored will be created and may not
+ * already exist.
+ *
+ * Example to restore a database.
+ *
+ *
{@code
+ * String backupInstanceId = my_instance_id;
+ * String backupId = my_backup_id;
+ * String restoreInstanceId = my_db_instance_id;
+ * String restoreDatabaseId = my_database_id;
+ * OperationFuture op = dbAdminClient
+ * .restoreDatabase(
+ * backupInstanceId,
+ * backupId,
+ * restoreInstanceId,
+ * restoreDatabaseId);
+ * Database database = op.get();
+ * }
+ *
+ * @param backupInstanceId the id of the instance where the backup is located.
+ * @param backupId the id of the backup to restore.
+ * @param restoreInstanceId the id of the instance where the database should be created. This may
+ * be a different instance than where the backup is stored.
+ * @param restoreDatabaseId the id of the database to restore to.
+ */
+ public OperationFuture restoreDatabase(
+ String backupInstanceId, String backupId, String restoreInstanceId, String restoreDatabaseId)
+ throws SpannerException;
+
+ /** Lists long-running database operations on the specified instance. */
+ Page listDatabaseOperations(String instanceId, ListOption... options);
+
+ /** Lists long-running backup operations on the specified instance. */
+ Page listBackupOperations(String instanceId, ListOption... options);
+
/**
* Gets the current state of a Cloud Spanner database.
*
@@ -77,6 +150,19 @@ OperationFuture createDatabase(
*/
Database getDatabase(String instanceId, String databaseId) throws SpannerException;
+ /**
+ * Gets the current state of a Cloud Spanner database backup.
+ *
+ * Example to get a backup.
+ *
+ *
{@code
+ * String instanceId = my_instance_id;
+ * String backupId = my_backup_id;
+ * Backup backup = dbAdminClient.getBackup(instanceId, backupId);
+ * }
+ */
+ Backup getBackup(String instanceId, String backupId) throws SpannerException;
+
/**
* Enqueues the given DDL statements to be applied, in order but not necessarily all at once, to
* the database schema at some point (or points) in the future. The server checks that the
@@ -153,6 +239,48 @@ OperationFuture updateDatabaseDdl(
*/
Page listDatabases(String instanceId, ListOption... options);
+ /**
+ * Returns the list of Cloud Spanner backups in the given instance.
+ *
+ * Example to get the list of Cloud Spanner backups in the given instance.
+ *
+ *
{@code
+ * String instanceId = my_instance_id;
+ * Page page = dbAdminClient.listBackups(instanceId, Options.pageSize(1));
+ * List backups = new ArrayList<>();
+ * while (page != null) {
+ * Backup backup = Iterables.getOnlyElement(page.getValues());
+ * dbs.add(backup);
+ * page = page.getNextPage();
+ * }
+ * }
+ */
+ Page listBackups(String instanceId, ListOption... options);
+
+ /**
+ * Updates the expire time of a backup.
+ *
+ * @param instanceId Required. The instance of the backup to update.
+ * @param backupId Required. The backup id of the backup to update.
+ * @param expireTime Required. The new expire time of the backup to set to.
+ * @return the updated Backup object.
+ */
+ Backup updateBackup(String instanceId, String backupId, Timestamp expireTime);
+
+ /**
+ * Deletes a pending or completed backup.
+ *
+ * @param instanceId Required. The instance where the backup exists.
+ * @param backupId Required. The id of the backup to delete.
+ */
+ void deleteBackup(String instanceId, String backupId);
+
+ /** Cancels the specified long-running operation. */
+ void cancelOperation(String name);
+
+ /** Gets the specified long-running operation. */
+ Operation getOperation(String name);
+
/** Returns the IAM policy for the given database. */
Policy getDatabaseIAMPolicy(String instanceId, String databaseId);
@@ -175,4 +303,27 @@ OperationFuture updateDatabaseDdl(
*/
Iterable testDatabaseIAMPermissions(
String instanceId, String databaseId, Iterable permissions);
+
+ /** Returns the IAM policy for the given backup. */
+ Policy getBackupIAMPolicy(String instanceId, String backupId);
+
+ /**
+ * Updates the IAM policy for the given backup and returns the resulting policy. It is highly
+ * recommended to first get the current policy and base the updated policy on the returned policy.
+ * See {@link Policy.Builder#setEtag(String)} for information on the recommended read-modify-write
+ * cycle.
+ */
+ Policy setBackupIAMPolicy(String instanceId, String backupId, Policy policy);
+
+ /**
+ * Tests for the given permissions on the specified backup for the caller.
+ *
+ * @param instanceId the id of the instance where the backup to test is located.
+ * @param backupId the id of the backup to test.
+ * @param permissions the permissions to test for. Permissions with wildcards (such as '*',
+ * 'spanner.*', 'spanner.instances.*') are not allowed.
+ * @return the subset of the tested permissions that the caller is allowed.
+ */
+ Iterable testBackupIAMPermissions(
+ String instanceId, String backupId, Iterable permissions);
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
index 3211c22b96..28de150cae 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
@@ -24,14 +24,19 @@
import com.google.api.gax.paging.Page;
import com.google.cloud.Policy;
import com.google.cloud.Policy.DefaultMarshaller;
+import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Options.ListOption;
import com.google.cloud.spanner.SpannerImpl.PageFetcher;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
+import com.google.longrunning.Operation;
import com.google.protobuf.Empty;
+import com.google.protobuf.FieldMask;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import java.util.List;
import java.util.UUID;
@@ -54,6 +59,7 @@ protected com.google.iam.v1.Policy toPb(Policy policy) {
private final String projectId;
private final SpannerRpc rpc;
private final PolicyMarshaller policyMarshaller = new PolicyMarshaller();
+ private static final String EXPIRE_TIME_MASK = "expire_time";
DatabaseAdminClientImpl(String projectId, SpannerRpc rpc) {
this.projectId = projectId;
@@ -66,6 +72,196 @@ private static String randomOperationId() {
return ("r" + uuid.toString()).replace("-", "_");
}
+ @Override
+ public Backup.Builder newBackupBuilder(BackupId backupId) {
+ return new Backup.Builder(this, backupId);
+ }
+
+ @Override
+ public OperationFuture restoreDatabase(
+ String backupInstanceId, String backupId, String restoreInstanceId, String restoreDatabaseId)
+ throws SpannerException {
+ String databaseInstanceName = getInstanceName(restoreInstanceId);
+ String backupName = getBackupName(backupInstanceId, backupId);
+
+ OperationFuture
+ rawOperationFuture =
+ rpc.restoreDatabase(databaseInstanceName, restoreDatabaseId, backupName);
+
+ return new OperationFutureImpl(
+ rawOperationFuture.getPollingFuture(),
+ rawOperationFuture.getInitialFuture(),
+ new ApiFunction() {
+ @Override
+ public Database apply(OperationSnapshot snapshot) {
+ return Database.fromProto(
+ ProtoOperationTransformers.ResponseTransformer.create(
+ com.google.spanner.admin.database.v1.Database.class)
+ .apply(snapshot),
+ DatabaseAdminClientImpl.this);
+ }
+ },
+ ProtoOperationTransformers.MetadataTransformer.create(RestoreDatabaseMetadata.class),
+ new ApiFunction() {
+ @Override
+ public Database apply(Exception e) {
+ throw SpannerExceptionFactory.newSpannerException(e);
+ }
+ });
+ }
+
+ @Override
+ public OperationFuture createBackup(
+ String instanceId, String backupId, String databaseId, Timestamp expireTime)
+ throws SpannerException {
+ com.google.spanner.admin.database.v1.Backup backup =
+ com.google.spanner.admin.database.v1.Backup.newBuilder()
+ .setDatabase(getDatabaseName(instanceId, databaseId))
+ .setExpireTime(expireTime.toProto())
+ .build();
+ String instanceName = getInstanceName(instanceId);
+ OperationFuture
+ rawOperationFuture = rpc.createBackup(instanceName, backupId, backup);
+
+ return new OperationFutureImpl(
+ rawOperationFuture.getPollingFuture(),
+ rawOperationFuture.getInitialFuture(),
+ new ApiFunction() {
+ @Override
+ public Backup apply(OperationSnapshot snapshot) {
+ com.google.spanner.admin.database.v1.Backup proto =
+ ProtoOperationTransformers.ResponseTransformer.create(
+ com.google.spanner.admin.database.v1.Backup.class)
+ .apply(snapshot);
+ return Backup.fromProto(
+ com.google.spanner.admin.database.v1.Backup.newBuilder(proto)
+ .setName(proto.getName())
+ .setExpireTime(proto.getExpireTime())
+ .setState(proto.getState())
+ .build(),
+ DatabaseAdminClientImpl.this);
+ }
+ },
+ ProtoOperationTransformers.MetadataTransformer.create(CreateBackupMetadata.class),
+ new ApiFunction() {
+ @Override
+ public Backup apply(Exception e) {
+ throw SpannerExceptionFactory.newSpannerException(e);
+ }
+ });
+ }
+
+ @Override
+ public Backup updateBackup(String instanceId, String backupId, Timestamp expireTime) {
+ String backupName = getBackupName(instanceId, backupId);
+ final com.google.spanner.admin.database.v1.Backup backup =
+ com.google.spanner.admin.database.v1.Backup.newBuilder()
+ .setName(backupName)
+ .setExpireTime(expireTime.toProto())
+ .build();
+ // Only update the expire time of the backup.
+ final FieldMask updateMask = FieldMask.newBuilder().addPaths(EXPIRE_TIME_MASK).build();
+ try {
+ return Backup.fromProto(rpc.updateBackup(backup, updateMask), DatabaseAdminClientImpl.this);
+ } catch (Exception e) {
+ throw SpannerExceptionFactory.newSpannerException(e);
+ }
+ }
+
+ @Override
+ public void deleteBackup(String instanceId, String backupId) {
+ final String backupName = getBackupName(instanceId, backupId);
+ try {
+ rpc.deleteBackup(backupName);
+ } catch (Exception e) {
+ throw SpannerExceptionFactory.newSpannerException(e);
+ }
+ }
+
+ @Override
+ public Backup getBackup(String instanceId, String backupId) throws SpannerException {
+ final String backupName = getBackupName(instanceId, backupId);
+ return Backup.fromProto(rpc.getBackup(backupName), DatabaseAdminClientImpl.this);
+ }
+
+ @Override
+ public final Page listBackupOperations(String instanceId, ListOption... options) {
+ final String instanceName = getInstanceName(instanceId);
+ final Options listOptions = Options.fromListOptions(options);
+ final int pageSize = listOptions.hasPageSize() ? listOptions.pageSize() : 0;
+ final String filter = listOptions.hasFilter() ? listOptions.filter() : null;
+ final String pageToken = listOptions.hasPageToken() ? listOptions.pageToken() : null;
+
+ PageFetcher pageFetcher =
+ new PageFetcher() {
+ @Override
+ public Paginated getNextPage(String nextPageToken) {
+ return rpc.listBackupOperations(instanceName, pageSize, filter, pageToken);
+ }
+
+ @Override
+ public Operation fromProto(Operation proto) {
+ return proto;
+ }
+ };
+ if (listOptions.hasPageToken()) {
+ pageFetcher.setNextPageToken(listOptions.pageToken());
+ }
+ return pageFetcher.getNextPage();
+ }
+
+ @Override
+ public final Page listDatabaseOperations(String instanceId, ListOption... options) {
+ final String instanceName = getInstanceName(instanceId);
+ final Options listOptions = Options.fromListOptions(options);
+ final int pageSize = listOptions.hasPageSize() ? listOptions.pageSize() : 0;
+ final String filter = listOptions.hasFilter() ? listOptions.filter() : null;
+ final String pageToken = listOptions.hasPageToken() ? listOptions.pageToken() : null;
+
+ PageFetcher pageFetcher =
+ new PageFetcher() {
+ @Override
+ public Paginated getNextPage(String nextPageToken) {
+ return rpc.listDatabaseOperations(instanceName, pageSize, filter, pageToken);
+ }
+
+ @Override
+ public Operation fromProto(Operation proto) {
+ return proto;
+ }
+ };
+ if (listOptions.hasPageToken()) {
+ pageFetcher.setNextPageToken(listOptions.pageToken());
+ }
+ return pageFetcher.getNextPage();
+ }
+
+ @Override
+ public Page listBackups(String instanceId, ListOption... options) {
+ final String instanceName = getInstanceName(instanceId);
+ final Options listOptions = Options.fromListOptions(options);
+ final String filter = listOptions.hasFilter() ? listOptions.filter() : null;
+ final int pageSize = listOptions.hasPageSize() ? listOptions.pageSize() : 0;
+
+ PageFetcher pageFetcher =
+ new PageFetcher() {
+ @Override
+ public Paginated getNextPage(
+ String nextPageToken) {
+ return rpc.listBackups(instanceName, pageSize, filter, nextPageToken);
+ }
+
+ @Override
+ public Backup fromProto(com.google.spanner.admin.database.v1.Backup proto) {
+ return Backup.fromProto(proto, DatabaseAdminClientImpl.this);
+ }
+ };
+ if (listOptions.hasPageToken()) {
+ pageFetcher.setNextPageToken(listOptions.pageToken());
+ }
+ return pageFetcher.getNextPage();
+ }
+
@Override
public OperationFuture createDatabase(
String instanceId, String databaseId, Iterable statements) throws SpannerException {
@@ -149,7 +345,7 @@ public Page listDatabases(String instanceId, ListOption... options) {
final String instanceName = getInstanceName(instanceId);
final Options listOptions = Options.fromListOptions(options);
Preconditions.checkArgument(
- !listOptions.hasFilter(), "Filter option is not support by" + "listDatabases");
+ !listOptions.hasFilter(), "Filter option is not supported by listDatabases");
final int pageSize = listOptions.hasPageSize() ? listOptions.pageSize() : 0;
PageFetcher pageFetcher =
new PageFetcher() {
@@ -181,6 +377,18 @@ public Database fromProto(com.google.spanner.admin.database.v1.Database proto) {
return pageFetcher.getNextPage();
}
+ @Override
+ public void cancelOperation(String name) {
+ Preconditions.checkNotNull(name);
+ rpc.cancelOperation(name);
+ }
+
+ @Override
+ public Operation getOperation(String name) {
+ Preconditions.checkNotNull(name);
+ return rpc.getOperation(name);
+ }
+
@Override
public Policy getDatabaseIAMPolicy(String instanceId, String databaseId) {
final String databaseName = DatabaseId.of(projectId, instanceId, databaseId).getName();
@@ -203,6 +411,28 @@ public Iterable testDatabaseIAMPermissions(
return rpc.testDatabaseAdminIAMPermissions(databaseName, permissions).getPermissionsList();
}
+ @Override
+ public Policy getBackupIAMPolicy(String instanceId, String backupId) {
+ final String databaseName = BackupId.of(projectId, instanceId, backupId).getName();
+ return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName));
+ }
+
+ @Override
+ public Policy setBackupIAMPolicy(String instanceId, String backupId, final Policy policy) {
+ Preconditions.checkNotNull(policy);
+ final String databaseName = BackupId.of(projectId, instanceId, backupId).getName();
+ return policyMarshaller.fromPb(
+ rpc.setDatabaseAdminIAMPolicy(databaseName, policyMarshaller.toPb(policy)));
+ }
+
+ @Override
+ public Iterable testBackupIAMPermissions(
+ String instanceId, String backupId, final Iterable permissions) {
+ Preconditions.checkNotNull(permissions);
+ final String databaseName = BackupId.of(projectId, instanceId, backupId).getName();
+ return rpc.testDatabaseAdminIAMPermissions(databaseName, permissions).getPermissionsList();
+ }
+
private String getInstanceName(String instanceId) {
return new InstanceId(projectId, instanceId).getName();
}
@@ -210,4 +440,9 @@ private String getInstanceName(String instanceId) {
private String getDatabaseName(String instanceId, String databaseId) {
return new DatabaseId(new InstanceId(projectId, instanceId), databaseId).getName();
}
+
+ private String getBackupName(String instanceId, String backupId) {
+ InstanceId instance = new InstanceId(projectId, instanceId);
+ return new BackupId(instance, backupId).getName();
+ }
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
index a45e180071..5b7faa324e 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
@@ -16,10 +16,69 @@
package com.google.cloud.spanner;
+import com.google.cloud.Timestamp;
+import com.google.common.base.Preconditions;
import java.util.Objects;
+import javax.annotation.Nullable;
/** Represents a Cloud Spanner database. */
public class DatabaseInfo {
+ public abstract static class Builder {
+ abstract Builder setState(State state);
+
+ abstract Builder setCreateTime(Timestamp createTime);
+
+ abstract Builder setRestoreInfo(RestoreInfo restoreInfo);
+
+ abstract Builder setProto(com.google.spanner.admin.database.v1.Database proto);
+
+ /** Builds the database from this builder. */
+ public abstract Database build();
+ }
+
+ abstract static class BuilderImpl extends Builder {
+ protected final DatabaseId id;
+ private State state = State.UNSPECIFIED;
+ private Timestamp createTime;
+ private RestoreInfo restoreInfo;
+ private com.google.spanner.admin.database.v1.Database proto;
+
+ BuilderImpl(DatabaseId id) {
+ this.id = Preconditions.checkNotNull(id);
+ }
+
+ BuilderImpl(DatabaseInfo other) {
+ this.id = other.id;
+ this.state = other.state;
+ this.createTime = other.createTime;
+ this.restoreInfo = other.restoreInfo;
+ this.proto = other.proto;
+ }
+
+ @Override
+ Builder setState(State state) {
+ this.state = Preconditions.checkNotNull(state);
+ return this;
+ }
+
+ @Override
+ Builder setCreateTime(Timestamp createTime) {
+ this.createTime = Preconditions.checkNotNull(createTime);
+ return this;
+ }
+
+ @Override
+ Builder setRestoreInfo(@Nullable RestoreInfo restoreInfo) {
+ this.restoreInfo = restoreInfo;
+ return this;
+ }
+
+ @Override
+ Builder setProto(@Nullable com.google.spanner.admin.database.v1.Database proto) {
+ this.proto = proto;
+ return this;
+ }
+ }
/** State of the database. */
public enum State {
@@ -28,15 +87,31 @@ public enum State {
// The database is still being created and is not ready to use.
CREATING,
// The database is fully created and ready to use.
- READY;
+ READY,
+ // The database has restored and is being optimized for use.
+ READY_OPTIMIZING
}
private final DatabaseId id;
private final State state;
+ private final Timestamp createTime;
+ private final RestoreInfo restoreInfo;
+ private final com.google.spanner.admin.database.v1.Database proto;
public DatabaseInfo(DatabaseId id, State state) {
this.id = id;
this.state = state;
+ this.createTime = null;
+ this.restoreInfo = null;
+ this.proto = null;
+ }
+
+ DatabaseInfo(BuilderImpl builder) {
+ this.id = builder.id;
+ this.state = builder.state;
+ this.createTime = builder.createTime;
+ this.restoreInfo = builder.restoreInfo;
+ this.proto = builder.proto;
}
/** Returns the database id. */
@@ -49,6 +124,24 @@ public State getState() {
return state;
}
+ /** Returns the creation time of the database. */
+ public Timestamp getCreateTime() {
+ return createTime;
+ }
+
+ /**
+ * Returns the {@link RestoreInfo} of the database if any is available, or null
if no
+ * {@link RestoreInfo} is available for this database.
+ */
+ public @Nullable RestoreInfo getRestoreInfo() {
+ return restoreInfo;
+ }
+
+ /** Returns the raw proto instance that was used to construct this {@link Database}. */
+ public @Nullable com.google.spanner.admin.database.v1.Database getProto() {
+ return proto;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -58,16 +151,19 @@ public boolean equals(Object o) {
return false;
}
DatabaseInfo that = (DatabaseInfo) o;
- return id.equals(that.id) && state == that.state;
+ return id.equals(that.id)
+ && state == that.state
+ && Objects.equals(createTime, that.createTime)
+ && Objects.equals(restoreInfo, that.restoreInfo);
}
@Override
public int hashCode() {
- return Objects.hash(id, state);
+ return Objects.hash(id, state, createTime, restoreInfo);
}
@Override
public String toString() {
- return String.format("Database[%s, %s]", id.getName(), state);
+ return String.format("Database[%s, %s, %s, %s]", id.getName(), state, createTime, restoreInfo);
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Instance.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Instance.java
index 5565c67de8..9484bd0590 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Instance.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Instance.java
@@ -19,6 +19,8 @@
import com.google.api.gax.longrunning.OperationFuture;
import com.google.api.gax.paging.Page;
import com.google.cloud.Policy;
+import com.google.cloud.spanner.Options.ListOption;
+import com.google.longrunning.Operation;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata;
import java.util.Map;
@@ -111,8 +113,8 @@ public OperationFuture update(
return instanceClient.updateInstance(this, fieldsToUpdate);
}
- public Page listDatabases() {
- return dbClient.listDatabases(instanceId());
+ public Page listDatabases(ListOption... options) {
+ return dbClient.listDatabases(instanceId(), options);
}
public Database getDatabase(String databaseId) {
@@ -132,6 +134,26 @@ public OperationFuture createDatabase(
return dbClient.createDatabase(instanceId(), databaseId, statements);
}
+ /** Returns the backups belonging to this instance. */
+ public Page listBackups(ListOption... options) {
+ return dbClient.listBackups(instanceId(), options);
+ }
+
+ /** Returns the backup with the given id on this instance. */
+ public Backup getBackup(String backupId) {
+ return dbClient.getBackup(instanceId(), backupId);
+ }
+
+ /** Returns the long-running database operations on this instance. */
+ public Page listDatabaseOperations(ListOption... options) {
+ return dbClient.listDatabaseOperations(instanceId(), options);
+ }
+
+ /** Returns the long-running backup operations on this instance. */
+ public Page listBackupOperations(ListOption... options) {
+ return dbClient.listBackupOperations(instanceId(), options);
+ }
+
/** Returns the IAM {@link Policy} for this instance. */
public Policy getIAMPolicy() {
return instanceClient.getInstanceIAMPolicy(instanceId());
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 e5d1729fef..d193ad1c75 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
@@ -314,5 +314,12 @@ static class FilterOption extends InternalOption implements ListOption {
void appendToOptions(Options options) {
options.filter = filter;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof FilterOption)) return false;
+ return Objects.equals(filter, ((FilterOption) o).filter);
+ }
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/RestoreInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/RestoreInfo.java
new file mode 100644
index 0000000000..f00504e698
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/RestoreInfo.java
@@ -0,0 +1,134 @@
+/*
+ * 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 javax.annotation.Nullable;
+
+/** Represents the restore information of a Cloud Spanner database. */
+public class RestoreInfo {
+ private static class Builder {
+ private final BackupId backup;
+ private RestoreSourceType sourceType;
+ private Timestamp backupCreateTime;
+ private DatabaseId sourceDatabase;
+ private com.google.spanner.admin.database.v1.RestoreInfo proto;
+
+ private Builder(BackupId backup) {
+ this.backup = backup;
+ }
+
+ private Builder setSourceType(RestoreSourceType sourceType) {
+ this.sourceType = sourceType;
+ return this;
+ }
+
+ private Builder setBackupCreateTime(Timestamp backupCreateTime) {
+ this.backupCreateTime = backupCreateTime;
+ return this;
+ }
+
+ private Builder setSourceDatabase(DatabaseId sourceDatabase) {
+ this.sourceDatabase = sourceDatabase;
+ return this;
+ }
+
+ private Builder setProto(com.google.spanner.admin.database.v1.RestoreInfo proto) {
+ this.proto = proto;
+ return this;
+ }
+
+ private RestoreInfo build() {
+ return new RestoreInfo(this);
+ }
+ }
+
+ /** Source of the restore information. */
+ public enum RestoreSourceType {
+ // Not specified.
+ UNSPECIFIED,
+ // The database was restored from a Backup.
+ BACKUP
+ }
+
+ private final BackupId backup;
+ private final RestoreSourceType sourceType;
+ private final Timestamp backupCreateTime;
+ private final DatabaseId sourceDatabase;
+ private final com.google.spanner.admin.database.v1.RestoreInfo proto;
+
+ private RestoreInfo(Builder builder) {
+ this.backup = builder.backup;
+ this.sourceType = builder.sourceType;
+ this.backupCreateTime = builder.backupCreateTime;
+ this.sourceDatabase = builder.sourceDatabase;
+ this.proto = builder.proto;
+ }
+
+ /** The backup source of the restored database. The backup may no longer exist. */
+ public BackupId getBackup() {
+ return backup;
+ }
+
+ /** The source type of the restore. */
+ public RestoreSourceType getSourceType() {
+ return sourceType;
+ }
+
+ /** The create time of the backup for the restore. */
+ public Timestamp getBackupCreateTime() {
+ return backupCreateTime;
+ }
+
+ /** The source database that was used to create the backup. The database may no longer exist. */
+ public DatabaseId getSourceDatabase() {
+ return sourceDatabase;
+ }
+
+ /** Returns the raw proto instance that was used to construct this {@link RestoreInfo}. */
+ public @Nullable com.google.spanner.admin.database.v1.RestoreInfo getProto() {
+ return proto;
+ }
+
+ /**
+ * Returns a {@link RestoreInfo} instance from the given proto, or null
if the given
+ * proto is the default proto instance (i.e. there is no restore info).
+ */
+ static RestoreInfo fromProtoOrNullIfDefaultInstance(
+ com.google.spanner.admin.database.v1.RestoreInfo proto) {
+ return proto.equals(com.google.spanner.admin.database.v1.RestoreInfo.getDefaultInstance())
+ ? null
+ : new Builder(BackupId.of(proto.getBackupInfo().getBackup()))
+ .setSourceType(fromProtoSourceType(proto.getSourceType()))
+ .setBackupCreateTime(Timestamp.fromProto(proto.getBackupInfo().getCreateTime()))
+ .setSourceDatabase(DatabaseId.of(proto.getBackupInfo().getSourceDatabase()))
+ .setProto(proto)
+ .build();
+ }
+
+ static RestoreSourceType fromProtoSourceType(
+ com.google.spanner.admin.database.v1.RestoreSourceType protoSourceType) {
+ switch (protoSourceType) {
+ case BACKUP:
+ return RestoreSourceType.BACKUP;
+ case TYPE_UNSPECIFIED:
+ return RestoreSourceType.UNSPECIFIED;
+ default:
+ throw new IllegalArgumentException("Unrecognized source type " + protoSourceType);
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
index f898b8f88f..32dc3b7157 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
@@ -18,8 +18,11 @@
import com.google.api.core.ApiFunction;
import com.google.api.gax.grpc.GrpcInterceptorProvider;
+import com.google.api.gax.longrunning.OperationSnapshot;
+import com.google.api.gax.longrunning.OperationTimedPollAlgorithm;
import com.google.api.gax.retrying.RetrySettings;
import com.google.api.gax.rpc.TransportChannelProvider;
+import com.google.api.gax.rpc.UnaryCallSettings;
import com.google.cloud.NoCredentials;
import com.google.cloud.ServiceDefaults;
import com.google.cloud.ServiceOptions;
@@ -40,6 +43,9 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.spanner.admin.database.v1.CreateBackupRequest;
+import com.google.spanner.admin.database.v1.CreateDatabaseRequest;
+import com.google.spanner.admin.database.v1.RestoreDatabaseRequest;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import io.grpc.CallCredentials;
import io.grpc.ManagedChannelBuilder;
@@ -81,6 +87,7 @@ public class SpannerOptions extends ServiceOptions {
private final InstanceAdminStubSettings instanceAdminStubSettings;
private final DatabaseAdminStubSettings databaseAdminStubSettings;
private final Duration partitionedDmlTimeout;
+ private final boolean autoThrottleAdministrativeRequests;
/**
* These are the default {@link QueryOptions} defined by the user on this {@link SpannerOptions}.
*/
@@ -152,6 +159,7 @@ private SpannerOptions(Builder builder) {
throw SpannerExceptionFactory.newSpannerException(e);
}
partitionedDmlTimeout = builder.partitionedDmlTimeout;
+ autoThrottleAdministrativeRequests = builder.autoThrottleAdministrativeRequests;
defaultQueryOptions = builder.defaultQueryOptions;
envQueryOptions = builder.getEnvironmentQueryOptions();
if (envQueryOptions.equals(QueryOptions.getDefaultInstance())) {
@@ -226,11 +234,65 @@ public static class Builder
private DatabaseAdminStubSettings.Builder databaseAdminStubSettingsBuilder =
DatabaseAdminStubSettings.newBuilder();
private Duration partitionedDmlTimeout = Duration.ofHours(2L);
+ private boolean autoThrottleAdministrativeRequests = false;
private Map defaultQueryOptions = new HashMap<>();
private CallCredentialsProvider callCredentialsProvider;
private String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
- private Builder() {}
+ private Builder() {
+ // Manually set retry and polling settings that work.
+ OperationTimedPollAlgorithm longRunningPollingAlgorithm =
+ OperationTimedPollAlgorithm.create(
+ RetrySettings.newBuilder()
+ .setInitialRpcTimeout(Duration.ofSeconds(60L))
+ .setMaxRpcTimeout(Duration.ofSeconds(600L))
+ .setInitialRetryDelay(Duration.ofSeconds(20L))
+ .setMaxRetryDelay(Duration.ofSeconds(45L))
+ .setRetryDelayMultiplier(1.5)
+ .setRpcTimeoutMultiplier(1.5)
+ .setTotalTimeout(Duration.ofHours(48L))
+ .build());
+ RetrySettings longRunningRetrySettings =
+ RetrySettings.newBuilder()
+ .setInitialRpcTimeout(Duration.ofSeconds(60L))
+ .setMaxRpcTimeout(Duration.ofSeconds(600L))
+ .setInitialRetryDelay(Duration.ofSeconds(20L))
+ .setMaxRetryDelay(Duration.ofSeconds(45L))
+ .setRetryDelayMultiplier(1.5)
+ .setRpcTimeoutMultiplier(1.5)
+ .setTotalTimeout(Duration.ofHours(48L))
+ .build();
+ databaseAdminStubSettingsBuilder
+ .createDatabaseOperationSettings()
+ .setPollingAlgorithm(longRunningPollingAlgorithm)
+ .setInitialCallSettings(
+ UnaryCallSettings
+ .newUnaryCallSettingsBuilder()
+ .setRetrySettings(longRunningRetrySettings)
+ .build());
+ databaseAdminStubSettingsBuilder
+ .createBackupOperationSettings()
+ .setPollingAlgorithm(longRunningPollingAlgorithm)
+ .setInitialCallSettings(
+ UnaryCallSettings
+ .newUnaryCallSettingsBuilder()
+ .setRetrySettings(longRunningRetrySettings)
+ .build());
+ databaseAdminStubSettingsBuilder
+ .restoreDatabaseOperationSettings()
+ .setPollingAlgorithm(longRunningPollingAlgorithm)
+ .setInitialCallSettings(
+ UnaryCallSettings
+ .newUnaryCallSettingsBuilder()
+ .setRetrySettings(longRunningRetrySettings)
+ .build());
+ databaseAdminStubSettingsBuilder
+ .deleteBackupSettings()
+ .setRetrySettings(longRunningRetrySettings);
+ databaseAdminStubSettingsBuilder
+ .updateBackupSettings()
+ .setRetrySettings(longRunningRetrySettings);
+ }
Builder(SpannerOptions options) {
super(options);
@@ -242,6 +304,7 @@ private Builder() {}
this.instanceAdminStubSettingsBuilder = options.instanceAdminStubSettings.toBuilder();
this.databaseAdminStubSettingsBuilder = options.databaseAdminStubSettings.toBuilder();
this.partitionedDmlTimeout = options.partitionedDmlTimeout;
+ this.autoThrottleAdministrativeRequests = options.autoThrottleAdministrativeRequests;
this.defaultQueryOptions = options.defaultQueryOptions;
this.callCredentialsProvider = options.callCredentialsProvider;
this.channelProvider = options.channelProvider;
@@ -435,6 +498,22 @@ public Builder setPartitionedDmlTimeout(Duration timeout) {
return this;
}
+ /**
+ * Instructs the client library to automatically throttle the number of administrative requests
+ * if the rate of administrative requests generated by this {@link Spanner} instance will exceed
+ * the administrative limits Cloud Spanner. The default behavior is to not throttle any
+ * requests. If the limit is exceeded, Cloud Spanner will return a RESOURCE_EXHAUSTED error.
+ * More information on the administrative limits can be found here:
+ * https://cloud.google.com/spanner/quotas#administrative_limits. Setting this option is not a
+ * guarantee that the rate will never be exceeded, as this option will only throttle requests
+ * coming from this client. Additional requests from other clients could still cause the limit
+ * to be exceeded.
+ */
+ public Builder setAutoThrottleAdministrativeRequests() {
+ this.autoThrottleAdministrativeRequests = true;
+ return this;
+ }
+
/**
* Sets the default {@link QueryOptions} that will be used for all queries on the specified
* database. Query options can also be specified on a per-query basis and as environment
@@ -592,6 +671,10 @@ public Duration getPartitionedDmlTimeout() {
return partitionedDmlTimeout;
}
+ public boolean isAutoThrottleAdministrativeRequests() {
+ return autoThrottleAdministrativeRequests;
+ }
+
public CallCredentialsProvider getCallCredentialsProvider() {
return callCredentialsProvider;
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
index b3a6c9b92b..de1a09158c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
@@ -51,24 +51,40 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.RateLimiter;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.iam.v1.GetIamPolicyRequest;
import com.google.iam.v1.Policy;
import com.google.iam.v1.SetIamPolicyRequest;
import com.google.iam.v1.TestIamPermissionsRequest;
import com.google.iam.v1.TestIamPermissionsResponse;
+import com.google.longrunning.CancelOperationRequest;
import com.google.longrunning.GetOperationRequest;
import com.google.longrunning.Operation;
import com.google.protobuf.Empty;
import com.google.protobuf.FieldMask;
+import com.google.spanner.admin.database.v1.Backup;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
+import com.google.spanner.admin.database.v1.CreateBackupRequest;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseRequest;
import com.google.spanner.admin.database.v1.Database;
+import com.google.spanner.admin.database.v1.DeleteBackupRequest;
import com.google.spanner.admin.database.v1.DropDatabaseRequest;
+import com.google.spanner.admin.database.v1.GetBackupRequest;
import com.google.spanner.admin.database.v1.GetDatabaseDdlRequest;
import com.google.spanner.admin.database.v1.GetDatabaseRequest;
+import com.google.spanner.admin.database.v1.ListBackupOperationsRequest;
+import com.google.spanner.admin.database.v1.ListBackupOperationsResponse;
+import com.google.spanner.admin.database.v1.ListBackupsRequest;
+import com.google.spanner.admin.database.v1.ListBackupsResponse;
+import com.google.spanner.admin.database.v1.ListDatabaseOperationsRequest;
+import com.google.spanner.admin.database.v1.ListDatabaseOperationsResponse;
import com.google.spanner.admin.database.v1.ListDatabasesRequest;
import com.google.spanner.admin.database.v1.ListDatabasesResponse;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseRequest;
+import com.google.spanner.admin.database.v1.UpdateBackupRequest;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest;
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
@@ -110,6 +126,8 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -189,6 +207,11 @@ private synchronized void shutdown() {
private final ScheduledExecutorService spannerWatchdog;
+ private final boolean throttleAdministrativeRequests;
+ private static final double ADMINISTRATIVE_REQUESTS_RATE_LIMIT = 1.0D;
+ private static final ConcurrentMap ADMINISTRATIVE_REQUESTS_RATE_LIMITERS =
+ new ConcurrentHashMap();
+
public static GapicSpannerRpc create(SpannerOptions options) {
return new GapicSpannerRpc(options);
}
@@ -203,6 +226,11 @@ public GapicSpannerRpc(final SpannerOptions options) {
} catch (UnsupportedEncodingException e) { // Ignored.
}
this.projectName = projectNameStr;
+ this.throttleAdministrativeRequests = options.isAutoThrottleAdministrativeRequests();
+ if (throttleAdministrativeRequests) {
+ ADMINISTRATIVE_REQUESTS_RATE_LIMITERS.putIfAbsent(
+ projectNameStr, RateLimiter.create(ADMINISTRATIVE_REQUESTS_RATE_LIMIT));
+ }
// create a metadataProvider which combines both internal headers and
// per-method-call extra headers for channelProvider to inject the headers
@@ -307,6 +335,15 @@ public GapicSpannerRpc(final SpannerOptions options) {
}
}
+ private void acquireAdministrativeRequestsRateLimiter() {
+ if (throttleAdministrativeRequests) {
+ RateLimiter limiter = ADMINISTRATIVE_REQUESTS_RATE_LIMITERS.get(this.projectName);
+ if (limiter != null) {
+ limiter.acquire();
+ }
+ }
+ }
+
@Override
public Paginated listInstanceConfigs(int pageSize, @Nullable String pageToken)
throws SpannerException {
@@ -390,9 +427,72 @@ public void deleteInstance(String instanceName) throws SpannerException {
get(instanceAdminStub.deleteInstanceCallable().futureCall(request, context));
}
+ @Override
+ public Paginated listBackupOperations(
+ String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken) {
+ acquireAdministrativeRequestsRateLimiter();
+ ListBackupOperationsRequest.Builder requestBuilder =
+ ListBackupOperationsRequest.newBuilder().setParent(instanceName).setPageSize(pageSize);
+ if (filter != null) {
+ requestBuilder.setFilter(filter);
+ }
+ if (pageToken != null) {
+ requestBuilder.setPageToken(pageToken);
+ }
+ ListBackupOperationsRequest request = requestBuilder.build();
+
+ GrpcCallContext context = newCallContext(null, instanceName);
+ ListBackupOperationsResponse response =
+ get(databaseAdminStub.listBackupOperationsCallable().futureCall(request, context));
+ return new Paginated<>(response.getOperationsList(), response.getNextPageToken());
+ }
+
+ @Override
+ public Paginated listDatabaseOperations(
+ String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken) {
+ acquireAdministrativeRequestsRateLimiter();
+ ListDatabaseOperationsRequest.Builder requestBuilder =
+ ListDatabaseOperationsRequest.newBuilder().setParent(instanceName).setPageSize(pageSize);
+
+ if (filter != null) {
+ requestBuilder.setFilter(filter);
+ }
+ if (pageToken != null) {
+ requestBuilder.setPageToken(pageToken);
+ }
+ ListDatabaseOperationsRequest request = requestBuilder.build();
+
+ GrpcCallContext context = newCallContext(null, instanceName);
+ ListDatabaseOperationsResponse response =
+ get(databaseAdminStub.listDatabaseOperationsCallable().futureCall(request, context));
+ return new Paginated<>(response.getOperationsList(), response.getNextPageToken());
+ }
+
+ @Override
+ public Paginated listBackups(
+ String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken)
+ throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
+ ListBackupsRequest.Builder requestBuilder =
+ ListBackupsRequest.newBuilder().setParent(instanceName).setPageSize(pageSize);
+ if (filter != null) {
+ requestBuilder.setFilter(filter);
+ }
+ if (pageToken != null) {
+ requestBuilder.setPageToken(pageToken);
+ }
+ ListBackupsRequest request = requestBuilder.build();
+
+ GrpcCallContext context = newCallContext(null, instanceName);
+ ListBackupsResponse response =
+ get(databaseAdminStub.listBackupsCallable().futureCall(request, context));
+ return new Paginated<>(response.getBackupsList(), response.getNextPageToken());
+ }
+
@Override
public Paginated listDatabases(
String instanceName, int pageSize, @Nullable String pageToken) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
ListDatabasesRequest.Builder requestBuilder =
ListDatabasesRequest.newBuilder().setParent(instanceName).setPageSize(pageSize);
if (pageToken != null) {
@@ -410,6 +510,7 @@ public Paginated listDatabases(
public OperationFuture createDatabase(
String instanceName, String createDatabaseStatement, Iterable additionalStatements)
throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
CreateDatabaseRequest request =
CreateDatabaseRequest.newBuilder()
.setParent(instanceName)
@@ -424,6 +525,7 @@ public OperationFuture createDatabase(
public OperationFuture updateDatabaseDdl(
String databaseName, Iterable updateDatabaseStatements, @Nullable String updateId)
throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
UpdateDatabaseDdlRequest request =
UpdateDatabaseDdlRequest.newBuilder()
.setDatabase(databaseName)
@@ -452,6 +554,7 @@ public OperationFuture updateDatabaseDdl(
@Override
public void dropDatabase(String databaseName) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
DropDatabaseRequest request =
DropDatabaseRequest.newBuilder().setDatabase(databaseName).build();
@@ -461,6 +564,7 @@ public void dropDatabase(String databaseName) throws SpannerException {
@Override
public Database getDatabase(String databaseName) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
GetDatabaseRequest request = GetDatabaseRequest.newBuilder().setName(databaseName).build();
GrpcCallContext context = newCallContext(null, databaseName);
@@ -469,6 +573,7 @@ public Database getDatabase(String databaseName) throws SpannerException {
@Override
public List getDatabaseDdl(String databaseName) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
GetDatabaseDdlRequest request =
GetDatabaseDdlRequest.newBuilder().setDatabase(databaseName).build();
@@ -477,14 +582,80 @@ public List getDatabaseDdl(String databaseName) throws SpannerException
.getStatementsList();
}
+ @Override
+ public OperationFuture createBackup(
+ String instanceName, String backupId, Backup backup) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
+ CreateBackupRequest request =
+ CreateBackupRequest.newBuilder()
+ .setParent(instanceName)
+ .setBackupId(backupId)
+ .setBackup(backup)
+ .build();
+ GrpcCallContext context = newCallContext(null, instanceName);
+ return databaseAdminStub.createBackupOperationCallable().futureCall(request, context);
+ }
+
+ @Override
+ public final OperationFuture restoreDatabase(
+ String databaseInstanceName, String databaseId, String backupName) {
+ acquireAdministrativeRequestsRateLimiter();
+ RestoreDatabaseRequest request =
+ RestoreDatabaseRequest.newBuilder()
+ .setParent(databaseInstanceName)
+ .setDatabaseId(databaseId)
+ .setBackup(backupName)
+ .build();
+ GrpcCallContext context = newCallContext(null, databaseInstanceName);
+ return databaseAdminStub.restoreDatabaseOperationCallable().futureCall(request, context);
+ }
+
+ @Override
+ public final Backup updateBackup(Backup backup, FieldMask updateMask) {
+ acquireAdministrativeRequestsRateLimiter();
+ UpdateBackupRequest request =
+ UpdateBackupRequest.newBuilder().setBackup(backup).setUpdateMask(updateMask).build();
+ GrpcCallContext context = newCallContext(null, backup.getName());
+ return databaseAdminStub.updateBackupCallable().call(request, context);
+ }
+
+ @Override
+ public final void deleteBackup(String backupName) {
+ acquireAdministrativeRequestsRateLimiter();
+ DeleteBackupRequest request = DeleteBackupRequest.newBuilder().setName(backupName).build();
+ GrpcCallContext context = newCallContext(null, backupName);
+ databaseAdminStub.deleteBackupCallable().call(request, context);
+ }
+
+ @Override
+ public Backup getBackup(String backupName) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
+ GetBackupRequest request = GetBackupRequest.newBuilder().setName(backupName).build();
+ GrpcCallContext context = newCallContext(null, backupName);
+ return get(databaseAdminStub.getBackupCallable().futureCall(request, context));
+ }
+
@Override
public Operation getOperation(String name) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
GetOperationRequest request = GetOperationRequest.newBuilder().setName(name).build();
GrpcCallContext context = newCallContext(null, name);
return get(
databaseAdminStub.getOperationsStub().getOperationCallable().futureCall(request, context));
}
+ @Override
+ public void cancelOperation(String name) throws SpannerException {
+ acquireAdministrativeRequestsRateLimiter();
+ CancelOperationRequest request = CancelOperationRequest.newBuilder().setName(name).build();
+ GrpcCallContext context = newCallContext(null, name);
+ get(
+ databaseAdminStub
+ .getOperationsStub()
+ .cancelOperationCallable()
+ .futureCall(request, context));
+ }
+
@Override
public List batchCreateSessions(
String databaseName,
@@ -636,6 +807,7 @@ public PartitionResponse partitionRead(
@Override
public Policy getDatabaseAdminIAMPolicy(String resource) {
+ acquireAdministrativeRequestsRateLimiter();
GrpcCallContext context = newCallContext(null, resource);
return get(
databaseAdminStub
@@ -645,6 +817,7 @@ public Policy getDatabaseAdminIAMPolicy(String resource) {
@Override
public Policy setDatabaseAdminIAMPolicy(String resource, Policy policy) {
+ acquireAdministrativeRequestsRateLimiter();
GrpcCallContext context = newCallContext(null, resource);
return get(
databaseAdminStub
@@ -657,6 +830,7 @@ public Policy setDatabaseAdminIAMPolicy(String resource, Policy policy) {
@Override
public TestIamPermissionsResponse testDatabaseAdminIAMPermissions(
String resource, Iterable permissions) {
+ acquireAdministrativeRequestsRateLimiter();
GrpcCallContext context = newCallContext(null, resource);
return get(
databaseAdminStub
@@ -671,6 +845,7 @@ public TestIamPermissionsResponse testDatabaseAdminIAMPermissions(
@Override
public Policy getInstanceAdminIAMPolicy(String resource) {
+ acquireAdministrativeRequestsRateLimiter();
GrpcCallContext context = newCallContext(null, resource);
return get(
instanceAdminStub
@@ -680,6 +855,7 @@ public Policy getInstanceAdminIAMPolicy(String resource) {
@Override
public Policy setInstanceAdminIAMPolicy(String resource, Policy policy) {
+ acquireAdministrativeRequestsRateLimiter();
GrpcCallContext context = newCallContext(null, resource);
return get(
instanceAdminStub
@@ -692,6 +868,7 @@ public Policy setInstanceAdminIAMPolicy(String resource, Policy policy) {
@Override
public TestIamPermissionsResponse testInstanceAdminIAMPermissions(
String resource, Iterable permissions) {
+ acquireAdministrativeRequestsRateLimiter();
GrpcCallContext context = newCallContext(null, resource);
return get(
instanceAdminStub
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java
index 8a30cae194..497be948cc 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java
@@ -29,8 +29,11 @@
import com.google.longrunning.Operation;
import com.google.protobuf.Empty;
import com.google.protobuf.FieldMask;
+import com.google.spanner.admin.database.v1.Backup;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.Database;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
import com.google.spanner.admin.instance.v1.Instance;
@@ -205,10 +208,59 @@ OperationFuture updateDatabaseDdl(
Database getDatabase(String databaseName) throws SpannerException;
List getDatabaseDdl(String databaseName) throws SpannerException;
+ /** Lists the backups in the specified instance. */
+ Paginated listBackups(
+ String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken)
+ throws SpannerException;
+
+ /**
+ * Creates a new backup from the source database specified in the {@link Backup} instance.
+ *
+ * @param instanceName the name of the instance where the backup should be created.
+ * @param backupId the id of the backup to create.
+ * @param backup the backup to create. The database and expireTime fields of the backup must be
+ * filled.
+ * @return the operation that monitors the backup creation.
+ */
+ OperationFuture createBackup(
+ String instanceName, String backupId, Backup backup) throws SpannerException;
+
+ /**
+ * Restore a backup into the given database.
+ *
+ * @param instanceName Fully qualified name of instance where to restore the database
+ * @param databaseId DatabaseId to restore into
+ * @param backupName Fully qualified name of backup to restore from
+ */
+ OperationFuture restoreDatabase(
+ String instanceName, String databaseId, String backupName);
+
+ /** Gets the backup with the specified name. */
+ Backup getBackup(String backupName) throws SpannerException;
+
+ /** Updates the specified backup. The only supported field for updates is expireTime. */
+ Backup updateBackup(Backup backup, FieldMask updateMask);
+
+ /** List all long-running backup operations on the given instance. */
+ Paginated listBackupOperations(
+ String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken);
+
+ /**
+ * Deletes a pending or completed backup.
+ *
+ * @param backupName Required. The fully qualified name of the backup to delete.
+ */
+ void deleteBackup(String backupName);
+
+ Paginated listDatabaseOperations(
+ String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken);
/** Retrieves a long running operation. */
Operation getOperation(String name) throws SpannerException;
+ /** Cancels the specified long-running operation. */
+ void cancelOperation(String name) throws SpannerException;
+
List batchCreateSessions(
String databaseName,
int sessionCount,
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
index 9ab9e19435..a9fc8faf5f 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
@@ -133,7 +133,10 @@ public void cleanUp() {
*/
public static RemoteSpannerHelper create(InstanceId instanceId) throws Throwable {
SpannerOptions options =
- SpannerOptions.newBuilder().setProjectId(instanceId.getProject()).build();
+ SpannerOptions.newBuilder()
+ .setProjectId(instanceId.getProject())
+ .setAutoThrottleAdministrativeRequests()
+ .build();
Spanner client = options.getService();
return new RemoteSpannerHelper(options, instanceId, client);
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupIdTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupIdTest.java
new file mode 100644
index 0000000000..c3405a05a0
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupIdTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link com.google.cloud.spanner.BackupId}. */
+@RunWith(JUnit4.class)
+public class BackupIdTest {
+ @Rule public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void basics() {
+ String name = "projects/test-project/instances/test-instance/backups/backup-1";
+ BackupId bid = BackupId.of(name);
+ assertThat(bid.getName()).isEqualTo(name);
+ assertThat(bid.getInstanceId().getInstance()).isEqualTo("test-instance");
+ assertThat(bid.getBackup()).isEqualTo("backup-1");
+ assertThat(BackupId.of("test-project", "test-instance", "backup-1")).isEqualTo(bid);
+ assertThat(BackupId.of(name)).isEqualTo(bid);
+ assertThat(BackupId.of(name).hashCode()).isEqualTo(bid.hashCode());
+ assertThat(bid.toString()).isEqualTo(name);
+ }
+
+ @Test
+ public void badName() {
+ try {
+ BackupId.of("bad name");
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage().contains("projects"));
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
new file mode 100644
index 0000000000..0e5e1afd39
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.google.cloud.Identity;
+import com.google.cloud.Policy;
+import com.google.cloud.Role;
+import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.Backup.Builder;
+import com.google.cloud.spanner.BackupInfo.State;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@RunWith(JUnit4.class)
+public class BackupTest {
+ private static final String NAME =
+ "projects/test-project/instances/test-instance/backups/backup-1";
+ private static final String DB = "projects/test-project/instances/test-instance/databases/db-1";
+ private static final Timestamp EXP_TIME = Timestamp.ofTimeSecondsAndNanos(1000L, 1000);
+
+ @Rule public ExpectedException expectedException = ExpectedException.none();
+ @Mock DatabaseAdminClient dbClient;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ when(dbClient.newBackupBuilder(Mockito.any(BackupId.class)))
+ .thenAnswer(
+ new Answer() {
+ @Override
+ public Builder answer(InvocationOnMock invocation) throws Throwable {
+ return new Backup.Builder(dbClient, (BackupId) invocation.getArguments()[0]);
+ }
+ });
+ }
+
+ @Test
+ public void build() {
+ Timestamp expireTime = Timestamp.now();
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id"))
+ .setDatabase(DatabaseId.of("test-project", "instance-id", "src-database"))
+ .setExpireTime(expireTime)
+ .setSize(100L)
+ .setState(State.CREATING)
+ .build();
+ Backup copy = backup.toBuilder().build();
+ assertThat(copy.getId()).isEqualTo(backup.getId());
+ assertThat(copy.getDatabase()).isEqualTo(backup.getDatabase());
+ assertThat(copy.getExpireTime()).isEqualTo(backup.getExpireTime());
+ assertThat(copy.getSize()).isEqualTo(backup.getSize());
+ assertThat(copy.getState()).isEqualTo(backup.getState());
+ }
+
+ @Test
+ public void create() {
+ Timestamp expireTime = Timestamp.now();
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id"))
+ .setDatabase(DatabaseId.of("test-project", "instance-id", "src-database"))
+ .setExpireTime(expireTime)
+ .build();
+ backup.create();
+ verify(dbClient).createBackup("instance-id", "backup-id", "src-database", expireTime);
+ }
+
+ @Test
+ public void createWithoutSource() {
+ Timestamp expireTime = Timestamp.now();
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "dest-instance", "backup-id"))
+ .setExpireTime(expireTime)
+ .build();
+ expectedException.expect(IllegalStateException.class);
+ backup.create();
+ }
+
+ @Test
+ public void createWithoutExpireTime() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id"))
+ .setDatabase(DatabaseId.of("test-project", "instance-id", "src-database"))
+ .build();
+ expectedException.expect(IllegalStateException.class);
+ backup.create();
+ }
+
+ @Test
+ public void exists() {
+ when(dbClient.getBackup("test-instance", "test-backup"))
+ .thenReturn(
+ new Backup.Builder(
+ dbClient, BackupId.of("test-project", "test-instance", "test-backup"))
+ .build());
+ when(dbClient.getBackup("other-instance", "other-backup"))
+ .thenThrow(
+ SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "backup not found"));
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ assertThat(backup.exists()).isTrue();
+ Backup otherBackup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "other-instance", "other-backup"))
+ .build();
+ assertThat(otherBackup.exists()).isFalse();
+ }
+
+ @Test
+ public void isReady() {
+ when(dbClient.getBackup("test-instance", "test-backup"))
+ .thenReturn(
+ new Backup.Builder(
+ dbClient, BackupId.of("test-project", "test-instance", "test-backup"))
+ .setState(State.READY)
+ .build());
+ when(dbClient.getBackup("other-instance", "other-backup"))
+ .thenReturn(
+ new Backup.Builder(
+ dbClient, BackupId.of("test-project", "other-instance", "other-backup"))
+ .setState(State.CREATING)
+ .build());
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .setState(State.UNSPECIFIED)
+ .build();
+ assertThat(backup.isReady()).isTrue();
+ assertThat(backup.getState()).isEqualTo(State.UNSPECIFIED);
+ Backup otherBackup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "other-instance", "other-backup"))
+ .setState(State.READY)
+ .build();
+ assertThat(otherBackup.isReady()).isFalse();
+ assertThat(otherBackup.getState()).isEqualTo(State.READY);
+ }
+
+ @Test
+ public void reload() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ backup.reload();
+ verify(dbClient).getBackup("test-instance", "test-backup");
+ }
+
+ @Test
+ public void delete() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ backup.delete();
+ verify(dbClient).deleteBackup("test-instance", "test-backup");
+ }
+
+ @Test
+ public void updateExpireTime() {
+ Timestamp expireTime = Timestamp.now();
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .setExpireTime(expireTime)
+ .build();
+ backup.updateExpireTime();
+ verify(dbClient).updateBackup("test-instance", "test-backup", expireTime);
+ }
+
+ @Test
+ public void updateExpireTimeWithoutExpireTime() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ expectedException.expect(IllegalStateException.class);
+ backup.updateExpireTime();
+ }
+
+ @Test
+ public void restore() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "backup-instance", "test-backup"))
+ .build();
+ backup.restore(DatabaseId.of("test-project", "db-instance", "test-database"));
+ verify(dbClient)
+ .restoreDatabase("backup-instance", "test-backup", "db-instance", "test-database");
+ }
+
+ @Test
+ public void restoreWithoutDestination() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ expectedException.expect(NullPointerException.class);
+ backup.restore(null);
+ }
+
+ @Test
+ public void listBackupOperations() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "backup-id"))
+ .build();
+ backup.listBackupOperations();
+ verify(dbClient)
+ .listBackupOperations("test-instance", Options.filter("name:backups/backup-id"));
+ }
+
+ @Test
+ public void getIAMPolicy() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ backup.getIAMPolicy();
+ verify(dbClient).getBackupIAMPolicy("test-instance", "test-backup");
+ }
+
+ @Test
+ public void setIAMPolicy() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ Policy policy =
+ Policy.newBuilder().addIdentity(Role.editor(), Identity.user("joe@example.com")).build();
+ backup.setIAMPolicy(policy);
+ verify(dbClient).setBackupIAMPolicy("test-instance", "test-backup", policy);
+ }
+
+ @Test
+ public void testIAMPermissions() {
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .build();
+ Iterable permissions = Arrays.asList("read");
+ backup.testIAMPermissions(permissions);
+ verify(dbClient).testBackupIAMPermissions("test-instance", "test-backup", permissions);
+ }
+
+ @Test
+ public void fromProto() {
+ Backup backup = createBackup();
+ assertThat(backup.getId().getName()).isEqualTo(NAME);
+ assertThat(backup.getState()).isEqualTo(BackupInfo.State.CREATING);
+ assertThat(backup.getExpireTime()).isEqualTo(EXP_TIME);
+ }
+
+ private Backup createBackup() {
+ com.google.spanner.admin.database.v1.Backup proto =
+ com.google.spanner.admin.database.v1.Backup.newBuilder()
+ .setName(NAME)
+ .setDatabase(DB)
+ .setExpireTime(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(1000L).setNanos(1000).build())
+ .setState(com.google.spanner.admin.database.v1.Backup.State.CREATING)
+ .build();
+ return Backup.fromProto(proto, dbClient);
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java
index 260c2f77ca..e77fb38439 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java
@@ -24,6 +24,7 @@
import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.Identity;
import com.google.cloud.Role;
+import com.google.cloud.Timestamp;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated;
import com.google.common.collect.ImmutableList;
@@ -36,13 +37,18 @@
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.Empty;
+import com.google.protobuf.FieldMask;
import com.google.protobuf.Message;
+import com.google.spanner.admin.database.v1.Backup;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.Database;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -60,6 +66,9 @@ public class DatabaseAdminClientImplTest {
private static final String DB_NAME = "projects/my-project/instances/my-instance/databases/my-db";
private static final String DB_NAME2 =
"projects/my-project/instances/my-instance/databases/my-db2";
+ private static final String BK_ID = "my-bk";
+ private static final String BK_NAME = "projects/my-project/instances/my-instance/backups/my-bk";
+ private static final String BK_NAME2 = "projects/my-project/instances/my-instance/backups/my-bk2";
@Mock SpannerRpc rpc;
DatabaseAdminClientImpl client;
@@ -85,6 +94,22 @@ static Any toAny(Message message) {
.build();
}
+ private Backup getBackupProto() {
+ return Backup.newBuilder()
+ .setName(BK_NAME)
+ .setDatabase(DB_NAME)
+ .setState(Backup.State.READY)
+ .build();
+ }
+
+ private Backup getAnotherBackupProto() {
+ return Backup.newBuilder()
+ .setName(BK_NAME2)
+ .setDatabase(DB_NAME2)
+ .setState(Backup.State.READY)
+ .build();
+ }
+
@Test
public void getDatabase() {
when(rpc.getDatabase(DB_NAME)).thenReturn(getDatabaseProto());
@@ -132,12 +157,7 @@ public void updateDatabaseDdlOpAlreadyExists() throws Exception {
Empty.getDefaultInstance(),
UpdateDatabaseDdlMetadata.getDefaultInstance());
- String newOpName = DB_NAME + "/operations/newop";
String newOpId = "newop";
- OperationFuture newop =
- OperationFutureUtil.immediateOperationFuture(
- newOpName, Empty.getDefaultInstance(), UpdateDatabaseDdlMetadata.getDefaultInstance());
-
when(rpc.updateDatabaseDdl(DB_NAME, ddl, newOpId)).thenReturn(originalOp);
OperationFuture op =
client.updateDatabaseDdl(INSTANCE_ID, DB_ID, ddl, newOpId);
@@ -270,4 +290,80 @@ public void testDatabaseIAMPermissions() {
Iterable allowed = client.testDatabaseIAMPermissions(INSTANCE_ID, DB_ID, permissions);
assertThat(allowed).containsExactly("spanner.databases.select");
}
+
+ @Test
+ public void createBackup() throws Exception {
+ OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "createBackup", getBackupProto(), CreateBackupMetadata.getDefaultInstance());
+ Timestamp t =
+ Timestamp.ofTimeMicroseconds(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
+ + TimeUnit.HOURS.toMicros(28));
+ Backup backup = Backup.newBuilder().setDatabase(DB_NAME).setExpireTime(t.toProto()).build();
+ when(rpc.createBackup(INSTANCE_NAME, BK_ID, backup)).thenReturn(rawOperationFuture);
+ OperationFuture op =
+ client.createBackup(INSTANCE_ID, BK_ID, DB_ID, t);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
+ }
+
+ @Test
+ public void deleteBackup() {
+ client.deleteBackup(INSTANCE_ID, BK_ID);
+ verify(rpc).deleteBackup(BK_NAME);
+ }
+
+ @Test
+ public void getBackup() {
+ when(rpc.getBackup(BK_NAME)).thenReturn(getBackupProto());
+ com.google.cloud.spanner.Backup bk = client.getBackup(INSTANCE_ID, BK_ID);
+ BackupId bid = BackupId.of(bk.getId().getName());
+ assertThat(bid.getName()).isEqualTo(BK_NAME);
+ assertThat(bk.getState()).isEqualTo(com.google.cloud.spanner.Backup.State.READY);
+ }
+
+ @Test
+ public void listBackups() {
+ String pageToken = "token";
+ when(rpc.listBackups(INSTANCE_NAME, 1, null, null))
+ .thenReturn(new Paginated<>(ImmutableList.of(getBackupProto()), pageToken));
+ when(rpc.listBackups(INSTANCE_NAME, 1, null, pageToken))
+ .thenReturn(new Paginated<>(ImmutableList.of(getAnotherBackupProto()), ""));
+ List backups =
+ Lists.newArrayList(client.listBackups(INSTANCE_ID, Options.pageSize(1)).iterateAll());
+ assertThat(backups.get(0).getId().getName()).isEqualTo(BK_NAME);
+ assertThat(backups.get(1).getId().getName()).isEqualTo(BK_NAME2);
+ assertThat(backups.size()).isEqualTo(2);
+ }
+
+ @Test
+ public void updateBackup() throws Exception {
+ Timestamp t =
+ Timestamp.ofTimeMicroseconds(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
+ + TimeUnit.HOURS.toMicros(28));
+ Backup backup = Backup.newBuilder().setName(BK_NAME).setExpireTime(t.toProto()).build();
+ when(rpc.updateBackup(backup, FieldMask.newBuilder().addPaths("expire_time").build()))
+ .thenReturn(
+ Backup.newBuilder()
+ .setName(BK_NAME)
+ .setDatabase(DB_NAME)
+ .setExpireTime(t.toProto())
+ .build());
+ com.google.cloud.spanner.Backup updatedBackup = client.updateBackup(INSTANCE_ID, BK_ID, t);
+ assertThat(updatedBackup.getExpireTime()).isEqualTo(t);
+ }
+
+ @Test
+ public void restoreDatabase() throws Exception {
+ OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "restoreDatabase", getDatabaseProto(), RestoreDatabaseMetadata.getDefaultInstance());
+ when(rpc.restoreDatabase(INSTANCE_NAME, DB_ID, BK_NAME)).thenReturn(rawOperationFuture);
+ OperationFuture op =
+ client.restoreDatabase(INSTANCE_ID, BK_ID, INSTANCE_ID, DB_ID);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(DB_NAME);
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java
index 1204908864..38bcd8437d 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java
@@ -16,21 +16,40 @@
package com.google.cloud.spanner;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import com.google.api.gax.grpc.testing.LocalChannelProvider;
import com.google.api.gax.grpc.testing.MockGrpcService;
import com.google.api.gax.grpc.testing.MockServiceHelper;
+import com.google.api.gax.longrunning.OperationFuture;
+import com.google.api.gax.longrunning.OperationSnapshot;
import com.google.api.gax.longrunning.OperationTimedPollAlgorithm;
+import com.google.api.gax.paging.Page;
import com.google.api.gax.retrying.RetrySettings;
+import com.google.api.gax.retrying.RetryingFuture;
import com.google.cloud.Identity;
import com.google.cloud.NoCredentials;
import com.google.cloud.Policy;
import com.google.cloud.Role;
+import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.DatabaseInfo.State;
+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 java.io.IOException;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -44,9 +63,42 @@
@RunWith(JUnit4.class)
public class DatabaseAdminClientTest {
+ private static class SpannerExecutionExceptionMatcher extends BaseMatcher {
+ private final ErrorCode expectedCode;
+
+ private static SpannerExecutionExceptionMatcher forCode(ErrorCode code) {
+ return new SpannerExecutionExceptionMatcher(code);
+ }
+
+ private SpannerExecutionExceptionMatcher(ErrorCode code) {
+ this.expectedCode = checkNotNull(code);
+ }
+
+ @Override
+ public boolean matches(Object item) {
+ if (item instanceof ExecutionException) {
+ ExecutionException e = (ExecutionException) item;
+ if (e.getCause() instanceof SpannerException) {
+ SpannerException se = (SpannerException) e.getCause();
+ return se.getErrorCode() == expectedCode;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("SpannerException[" + expectedCode + "]");
+ }
+ }
+
private static final String PROJECT_ID = "my-project";
private static final String INSTANCE_ID = "my-instance";
private static final String DB_ID = "test-db";
+ private static final String BCK_ID = "test-bck";
+ private static final String RESTORED_ID = "restored-test-db";
+ private static final String TEST_PARENT = "projects/my-project/instances/my-instance";
+ private static final String TEST_BCK_NAME = String.format("%s/backups/test-bck", TEST_PARENT);
private static final List INITIAL_STATEMENTS =
Arrays.asList("CREATE TABLE FOO", "CREATE TABLE BAR");
@@ -57,6 +109,9 @@ public class DatabaseAdminClientTest {
private Spanner spanner;
private DatabaseAdminClient client;
@Rule public ExpectedException exception = ExpectedException.none();
+ private OperationFuture createDatabaseOperation;
+ private OperationFuture createBackupOperation;
+ private OperationFuture restoreDatabaseOperation;
@BeforeClass
public static void startStaticServer() {
@@ -78,6 +133,21 @@ public void setUp() throws IOException {
serviceHelper.reset();
channelProvider = serviceHelper.createChannelProvider();
SpannerOptions.Builder builder = SpannerOptions.newBuilder();
+ builder
+ .getDatabaseAdminStubSettingsBuilder()
+ .createBackupOperationSettings()
+ .setPollingAlgorithm(
+ OperationTimedPollAlgorithm.create(
+ RetrySettings.newBuilder()
+ .setInitialRpcTimeout(Duration.ofMillis(20L))
+ .setInitialRetryDelay(Duration.ofMillis(10L))
+ .setMaxRetryDelay(Duration.ofMillis(150L))
+ .setMaxRpcTimeout(Duration.ofMillis(150L))
+ .setMaxAttempts(10)
+ .setTotalTimeout(Duration.ofMillis(5000L))
+ .setRetryDelayMultiplier(1.3)
+ .setRpcTimeoutMultiplier(1.3)
+ .build()));
builder
.getDatabaseAdminStubSettingsBuilder()
.createDatabaseOperationSettings()
@@ -93,6 +163,21 @@ public void setUp() throws IOException {
.setRetryDelayMultiplier(1.3)
.setRpcTimeoutMultiplier(1.3)
.build()));
+ builder
+ .getDatabaseAdminStubSettingsBuilder()
+ .restoreDatabaseOperationSettings()
+ .setPollingAlgorithm(
+ OperationTimedPollAlgorithm.create(
+ RetrySettings.newBuilder()
+ .setInitialRpcTimeout(Duration.ofMillis(20L))
+ .setInitialRetryDelay(Duration.ofMillis(10L))
+ .setMaxRetryDelay(Duration.ofMillis(150L))
+ .setMaxRpcTimeout(Duration.ofMillis(150L))
+ .setMaxAttempts(10)
+ .setTotalTimeout(Duration.ofMillis(5000L))
+ .setRetryDelayMultiplier(1.3)
+ .setRpcTimeoutMultiplier(1.3)
+ .build()));
spanner =
builder
.setChannelProvider(channelProvider)
@@ -102,13 +187,523 @@ public void setUp() throws IOException {
.getService();
client = spanner.getDatabaseAdminClient();
createTestDatabase();
+ createTestBackup();
+ restoreTestBackup();
}
@After
public void tearDown() throws Exception {
+ serviceHelper.reset();
spanner.close();
}
+ @Test
+ public void dbAdminCreateBackup() throws InterruptedException, ExecutionException {
+ final String backupId = "other-backup-id";
+ OperationFuture op =
+ client.createBackup(INSTANCE_ID, backupId, DB_ID, after7Days());
+ Backup backup = op.get();
+ assertThat(backup.getId().getName())
+ .isEqualTo(
+ String.format(
+ "projects/%s/instances/%s/backups/%s", PROJECT_ID, INSTANCE_ID, backupId));
+ assertThat(client.getBackup(INSTANCE_ID, backupId)).isEqualTo(backup);
+ }
+
+ @Test
+ public void backupCreate() throws InterruptedException, ExecutionException {
+ final String backupId = "other-backup-id";
+ Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(after7Days())
+ .build();
+ OperationFuture op = backup.create();
+ backup = op.get();
+ assertThat(backup.getId().getName())
+ .isEqualTo(
+ String.format(
+ "projects/%s/instances/%s/backups/%s", PROJECT_ID, INSTANCE_ID, backupId));
+ assertThat(client.getBackup(INSTANCE_ID, backupId)).isEqualTo(backup);
+ }
+
+ @Test
+ public void backupCreateCancel() {
+ final String backupId = "other-backup-id";
+ // Set expire time to 14 days from now.
+ long currentTimeInMicroSeconds =
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+ long deltaTimeInMicroseconds = TimeUnit.MICROSECONDS.convert(14L, TimeUnit.DAYS);
+ Timestamp expireTime =
+ Timestamp.ofTimeMicroseconds(currentTimeInMicroSeconds + deltaTimeInMicroseconds);
+ Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(expireTime)
+ .build();
+ // Start a creation of a backup.
+ OperationFuture op = backup.create();
+ try {
+ // Try to cancel the backup operation.
+ client.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 if the operation has finished successfully or if it was cancelled
+ // or any other error occurred.
+ while (!pollingFuture.get().isDone()) {
+ Thread.sleep(TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS));
+ }
+ } catch (CancellationException e) {
+ // ignore, this exception may also occur if the polling future has been cancelled.
+ } catch (ExecutionException e) {
+ throw (RuntimeException) e.getCause();
+ } catch (InterruptedException e) {
+ throw SpannerExceptionFactory.propagateInterrupt(e);
+ } finally {
+ backup.delete();
+ }
+ }
+
+ @Test
+ public void databaseBackup() throws InterruptedException, ExecutionException {
+ final String backupId = "other-backup-id";
+ Database db = client.getDatabase(INSTANCE_ID, DB_ID);
+ Backup backup =
+ db.backup(
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
+ .setExpireTime(after7Days())
+ .build())
+ .get();
+ assertThat(backup.getId().getName())
+ .isEqualTo(
+ String.format(
+ "projects/%s/instances/%s/backups/%s", PROJECT_ID, INSTANCE_ID, backupId));
+ assertThat(client.getBackup(INSTANCE_ID, backupId)).isEqualTo(backup);
+ }
+
+ @Test
+ public void dbAdminCreateBackupAlreadyExists() throws InterruptedException, ExecutionException {
+ OperationFuture op =
+ client.createBackup(INSTANCE_ID, BCK_ID, DB_ID, after7Days());
+ exception.expect(SpannerExecutionExceptionMatcher.forCode(ErrorCode.ALREADY_EXISTS));
+ op.get();
+ }
+
+ @Test
+ public void backupCreateAlreadyExists() throws InterruptedException, ExecutionException {
+ Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(after7Days())
+ .build();
+ OperationFuture op = backup.create();
+ exception.expect(SpannerExecutionExceptionMatcher.forCode(ErrorCode.ALREADY_EXISTS));
+ op.get();
+ }
+
+ @Test
+ public void databaseBackupAlreadyExists() throws InterruptedException, ExecutionException {
+ Database db = client.getDatabase(INSTANCE_ID, DB_ID);
+ OperationFuture op =
+ db.backup(
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID))
+ .setExpireTime(after7Days())
+ .build());
+ exception.expect(SpannerExecutionExceptionMatcher.forCode(ErrorCode.ALREADY_EXISTS));
+ op.get();
+ }
+
+ @Test
+ public void dbAdminCreateBackupDbNotFound() throws InterruptedException, ExecutionException {
+ final String backupId = "other-backup-id";
+ OperationFuture op =
+ client.createBackup(INSTANCE_ID, backupId, "does-not-exist", after7Days());
+ exception.expect(SpannerExecutionExceptionMatcher.forCode(ErrorCode.NOT_FOUND));
+ op.get();
+ }
+
+ @Test
+ public void backupCreateDbNotFound() throws InterruptedException, ExecutionException {
+ final String backupId = "other-backup-id";
+ Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, "does-not-exist"))
+ .setExpireTime(after7Days())
+ .build();
+ OperationFuture op = backup.create();
+ exception.expect(SpannerExecutionExceptionMatcher.forCode(ErrorCode.NOT_FOUND));
+ op.get();
+ }
+
+ @Test
+ public void databaseBackupDbNotFound() throws InterruptedException, ExecutionException {
+ final String backupId = "other-backup-id";
+ Database db =
+ new Database(
+ DatabaseId.of(PROJECT_ID, INSTANCE_ID, "does-not-exist"), State.UNSPECIFIED, client);
+ OperationFuture op =
+ db.backup(
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
+ .setExpireTime(after7Days())
+ .build());
+ exception.expect(SpannerExecutionExceptionMatcher.forCode(ErrorCode.NOT_FOUND));
+ op.get();
+ }
+
+ @Test
+ public void dbAdminDeleteBackup() {
+ Backup backup = client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)).build();
+ assertThat(backup.exists()).isTrue();
+ client.deleteBackup(INSTANCE_ID, BCK_ID);
+ assertThat(backup.exists()).isFalse();
+ }
+
+ @Test
+ public void backupDelete() {
+ Backup backup = client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)).build();
+ assertThat(backup.exists()).isTrue();
+ backup.delete();
+ assertThat(backup.exists()).isFalse();
+ }
+
+ @Test
+ public void dbAdminDeleteBackupNotFound() {
+ exception.expect(SpannerMatchers.isSpannerException(ErrorCode.NOT_FOUND));
+ client.deleteBackup(INSTANCE_ID, "does-not-exist");
+ }
+
+ @Test
+ public void backupDeleteNotFound() {
+ Backup backup =
+ client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, "does-not-exist")).build();
+ exception.expect(SpannerMatchers.isSpannerException(ErrorCode.NOT_FOUND));
+ backup.delete();
+ }
+
+ @Test
+ public void dbAdminGetBackup() {
+ Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
+ assertThat(backup.getId().getName()).isEqualTo(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void backupReload() {
+ Backup backup = client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)).build();
+ assertThat(backup.getState()).isEqualTo(com.google.cloud.spanner.BackupInfo.State.UNSPECIFIED);
+ backup.reload();
+ assertThat(backup.getId().getName()).isEqualTo(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void dbAdminGetBackupNotFound() {
+ exception.expect(SpannerMatchers.isSpannerException(ErrorCode.NOT_FOUND));
+ client.getBackup(INSTANCE_ID, "does-not-exist");
+ }
+
+ @Test
+ public void backupReloadNotFound() {
+ Backup backup =
+ client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, "does-not-exist")).build();
+ exception.expect(SpannerMatchers.isSpannerException(ErrorCode.NOT_FOUND));
+ backup.reload();
+ }
+
+ @Test
+ public void backupExists() {
+ Backup backup =
+ client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, "does-not-exist")).build();
+ assertThat(backup.exists()).isFalse();
+ backup = client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)).build();
+ assertThat(backup.exists()).isTrue();
+ }
+
+ @Test
+ public void dbClientListBackups()
+ throws SpannerException, InterruptedException, ExecutionException {
+ Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
+ assertThat(client.listBackups(INSTANCE_ID).iterateAll()).containsExactly(backup);
+ Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, after7Days()).get();
+ assertThat(client.listBackups(INSTANCE_ID).iterateAll()).containsExactly(backup, backup2);
+ backup2.delete();
+ assertThat(client.listBackups(INSTANCE_ID).iterateAll()).containsExactly(backup);
+ }
+
+ @Test
+ public void instanceListBackups()
+ throws SpannerException, InterruptedException, ExecutionException {
+ Instance instance =
+ spanner
+ .getInstanceAdminClient()
+ .newInstanceBuilder(InstanceId.of(PROJECT_ID, INSTANCE_ID))
+ .build();
+ Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
+ assertThat(instance.listBackups().iterateAll()).containsExactly(backup);
+ Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, after7Days()).get();
+ assertThat(instance.listBackups().iterateAll()).containsExactly(backup, backup2);
+ backup2.delete();
+ assertThat(instance.listBackups().iterateAll()).containsExactly(backup);
+ }
+
+ @Test
+ public void instanceListBackupsWithFilter()
+ throws SpannerException, InterruptedException, ExecutionException {
+ Instance instance =
+ spanner
+ .getInstanceAdminClient()
+ .newInstanceBuilder(InstanceId.of(PROJECT_ID, INSTANCE_ID))
+ .build();
+
+ Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
+ assertThat(instance.listBackups().iterateAll()).containsExactly(backup);
+ Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, after7Days()).get();
+
+ // All backups.
+ assertThat(instance.listBackups().iterateAll()).containsExactly(backup, backup2);
+
+ // All backups with name containing 'backup2'.
+ String filter = "name:backup2";
+ mockDatabaseAdmin.addFilterMatches(filter, backup2.getId().getName());
+ assertThat(instance.listBackups(Options.filter(filter)).iterateAll()).containsExactly(backup2);
+
+ // All backups for a database with the name db2.
+ filter = String.format("database:%s", DB_ID);
+ mockDatabaseAdmin.addFilterMatches(filter, backup.getId().getName(), backup2.getId().getName());
+ assertThat(instance.listBackups(Options.filter(filter)).iterateAll())
+ .containsExactly(backup, backup2);
+
+ // All backups that expire before a certain time.
+ String ts = after14Days().toString();
+ filter = String.format("expire_time < \"%s\"", ts);
+ mockDatabaseAdmin.addFilterMatches(filter, backup.getId().getName(), backup2.getId().getName());
+ assertThat(instance.listBackups(Options.filter(filter)).iterateAll())
+ .containsExactly(backup, backup2);
+ // All backups with size greater than a certain number of bytes.
+ long minBytes = Math.min(backup.getSize(), backup2.getSize());
+ filter = String.format("size_bytes > %d", minBytes);
+ Backup backupWithLargestSize;
+ if (backup.getSize() == minBytes) {
+ backupWithLargestSize = backup2;
+ } else {
+ backupWithLargestSize = backup;
+ }
+ mockDatabaseAdmin.addFilterMatches(filter, backupWithLargestSize.getId().getName());
+ assertThat(instance.listBackups(Options.filter(filter)).iterateAll())
+ .containsExactly(backupWithLargestSize);
+ // All backups with a create time after a certain timestamp and that are also ready.
+ ts = backup2.getProto().getCreateTime().toString();
+ filter = String.format("create_time >= \"%s\" AND state:READY", ts.toString());
+ mockDatabaseAdmin.addFilterMatches(filter, backup2.getId().getName());
+ assertThat(instance.listBackups(Options.filter(filter)).iterateAll()).containsExactly(backup2);
+ }
+
+ @Test
+ public void dbClientUpdateBackup() {
+ Timestamp oldExpireTime = client.getBackup(INSTANCE_ID, BCK_ID).getExpireTime();
+ Timestamp newExpireTime =
+ Timestamp.ofTimeSecondsAndNanos(
+ Timestamp.now().getSeconds() + TimeUnit.SECONDS.convert(1, TimeUnit.DAYS), 0);
+ assertThat(oldExpireTime).isNotEqualTo(newExpireTime);
+ Backup backup = client.updateBackup(INSTANCE_ID, BCK_ID, newExpireTime);
+ assertThat(backup.getExpireTime()).isEqualTo(newExpireTime);
+ assertThat(client.getBackup(INSTANCE_ID, BCK_ID)).isEqualTo(backup);
+ }
+
+ @Test
+ public void backupUpdate() {
+ Timestamp newExpireTime =
+ Timestamp.ofTimeSecondsAndNanos(
+ Timestamp.now().getSeconds() + TimeUnit.SECONDS.convert(1, TimeUnit.DAYS), 0);
+ Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
+ assertThat(backup.getExpireTime()).isNotEqualTo(newExpireTime);
+ backup.toBuilder().setExpireTime(newExpireTime).build().updateExpireTime();
+ Backup updated = client.getBackup(INSTANCE_ID, BCK_ID);
+ assertThat(updated.getExpireTime()).isEqualTo(newExpireTime);
+ assertThat(updated).isNotEqualTo(backup);
+ assertThat(backup.reload()).isEqualTo(updated);
+ }
+
+ @Test
+ public void dbClientRestoreDatabase() throws InterruptedException, ExecutionException {
+ OperationFuture op =
+ client.restoreDatabase(INSTANCE_ID, BCK_ID, "other-instance-id", "restored-db");
+ Database restored = op.get();
+ assertThat(restored.getId().getDatabase()).isEqualTo("restored-db");
+ assertThat(restored.getId().getInstanceId().getInstance()).isEqualTo("other-instance-id");
+ }
+
+ @Test
+ public void backupRestoreDatabase() throws InterruptedException, ExecutionException {
+ Backup backup = client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)).build();
+ Database restored =
+ backup.restore(DatabaseId.of(PROJECT_ID, "other-instance-id", "restored-db")).get();
+ assertThat(restored.getId().getDatabase()).isEqualTo("restored-db");
+ assertThat(restored.getId().getInstanceId().getInstance()).isEqualTo("other-instance-id");
+ }
+
+ @Test
+ public void dbClientListDatabaseOperations()
+ throws SpannerException, InterruptedException, ExecutionException {
+ // Note: The mock server keeps all operations until the server is reset, including operations
+ // that have already finished.
+ // The setup method creates a test database --> 1 operation.
+ // + restores a database --> 2 operations.
+ assertThat(client.listDatabaseOperations(INSTANCE_ID).iterateAll()).hasSize(3);
+ // Create another database which should also create another operation.
+ client.createDatabase(INSTANCE_ID, "other-database", Collections.emptyList()).get();
+ assertThat(client.listDatabaseOperations(INSTANCE_ID).iterateAll()).hasSize(4);
+ // Restore a backup. This should create 2 database operations: One to restore the database and
+ // one to optimize it.
+ client.restoreDatabase(INSTANCE_ID, BCK_ID, INSTANCE_ID, "restored-db").get();
+ assertThat(client.listDatabaseOperations(INSTANCE_ID).iterateAll()).hasSize(6);
+ }
+
+ @Test
+ public void instanceListDatabaseOperations()
+ throws SpannerException, InterruptedException, ExecutionException {
+ Instance instance =
+ spanner
+ .getInstanceAdminClient()
+ .newInstanceBuilder(InstanceId.of(PROJECT_ID, INSTANCE_ID))
+ .build();
+ assertThat(instance.listDatabaseOperations().iterateAll()).hasSize(3);
+ instance.createDatabase("other-database", Collections.emptyList()).get();
+ assertThat(instance.listDatabaseOperations().iterateAll()).hasSize(4);
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID))
+ .build()
+ .restore(DatabaseId.of(PROJECT_ID, INSTANCE_ID, "restored-db"))
+ .get();
+ assertThat(instance.listDatabaseOperations().iterateAll()).hasSize(6);
+ }
+
+ @Test
+ public void instanceListDatabaseOperationsWithMetadata() throws Exception {
+ Instance instance =
+ spanner
+ .getInstanceAdminClient()
+ .newInstanceBuilder(InstanceId.of(PROJECT_ID, INSTANCE_ID))
+ .build();
+ String filter =
+ "(metadata.@type:type.googleapis.com/"
+ + "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)";
+ mockDatabaseAdmin.addFilterMatches(
+ filter, restoreDatabaseOperation.getMetadata().get().getOptimizeDatabaseOperationName());
+ Iterable operations =
+ instance.listDatabaseOperations(Options.filter(filter)).iterateAll();
+ assertThat(operations).hasSize(1);
+ for (Operation op : operations) {
+ OptimizeRestoredDatabaseMetadata metadata =
+ op.getMetadata().unpack(OptimizeRestoredDatabaseMetadata.class);
+ String progress =
+ String.format(
+ "Restored database %s is optimized",
+ metadata.getName(), metadata.getProgress().getProgressPercent());
+ assertThat(progress.contains("100%"));
+ }
+ }
+
+ @Test
+ public void databaseListDatabaseOperations()
+ throws SpannerException, InterruptedException, ExecutionException {
+ Database database = client.getDatabase(INSTANCE_ID, DB_ID);
+ mockDatabaseAdmin.addFilterMatches(
+ "name:databases/" + DB_ID, createDatabaseOperation.getName());
+ assertThat(database.listDatabaseOperations().iterateAll()).hasSize(1);
+ // Create another database which should also create another operation, but for a different
+ // database.
+ client.createDatabase(INSTANCE_ID, "other-database", Collections.emptyList()).get();
+ assertThat(database.listDatabaseOperations().iterateAll()).hasSize(1);
+ // Update the database DDL. This should create an operation for this database.
+ OperationFuture op =
+ database.updateDdl(Arrays.asList("DROP TABLE FOO"), null);
+ mockDatabaseAdmin.addFilterMatches("name:databases/" + DB_ID, op.getName());
+ assertThat(database.listDatabaseOperations().iterateAll()).hasSize(2);
+ }
+
+ @Test
+ public void dbClientListBackupOperations()
+ throws SpannerException, InterruptedException, ExecutionException {
+ assertThat(client.listBackupOperations(INSTANCE_ID).iterateAll()).hasSize(1);
+ client.createBackup(INSTANCE_ID, "other-backup", DB_ID, after7Days()).get();
+ assertThat(client.listBackupOperations(INSTANCE_ID).iterateAll()).hasSize(2);
+ // Restore a backup. This creates 2 DATABASE operations: One to restore the database and
+ // one to optimize it.
+ client.restoreDatabase(INSTANCE_ID, BCK_ID, INSTANCE_ID, "restored-db").get();
+ assertThat(client.listBackupOperations(INSTANCE_ID).iterateAll()).hasSize(2);
+ }
+
+ @Test
+ public void instanceListBackupOperations()
+ throws SpannerException, InterruptedException, ExecutionException {
+ Instance instance =
+ spanner
+ .getInstanceAdminClient()
+ .newInstanceBuilder(InstanceId.of(PROJECT_ID, INSTANCE_ID))
+ .build();
+ assertThat(instance.listBackupOperations().iterateAll()).hasSize(1);
+ instance
+ .getDatabase(DB_ID)
+ .backup(
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, "other-backup"))
+ .setExpireTime(after7Days())
+ .build())
+ .get();
+ assertThat(instance.listBackupOperations().iterateAll()).hasSize(2);
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID))
+ .build()
+ .restore(DatabaseId.of(PROJECT_ID, INSTANCE_ID, "restored-db"))
+ .get();
+ assertThat(instance.listBackupOperations().iterateAll()).hasSize(2);
+ }
+
+ @Test
+ public void instanceListBackupOperationsWithProgress() throws InvalidProtocolBufferException {
+ Instance instance =
+ spanner
+ .getInstanceAdminClient()
+ .newInstanceBuilder(InstanceId.of(PROJECT_ID, INSTANCE_ID))
+ .build();
+ String database = String.format("%s/databases/%s", TEST_PARENT, DB_ID);
+ String filter =
+ String.format(
+ "(metadata.database:%s) AND "
+ + "(metadata.@type:type.googleapis.com/"
+ + "google.spanner.admin.database.v1.CreateBackupMetadata)",
+ database);
+ Page operations = instance.listBackupOperations(Options.filter(filter));
+ for (Operation op : operations.iterateAll()) {
+ CreateBackupMetadata metadata = op.getMetadata().unpack(CreateBackupMetadata.class);
+ String progress =
+ String.format(
+ "Backup %s on database %s pending: %d%% complete",
+ metadata.getName(),
+ metadata.getDatabase(),
+ metadata.getProgress().getProgressPercent());
+ assertThat(progress.contains("100%"));
+ }
+ }
+
+ @Test
+ public void backupListBackupOperations()
+ throws SpannerException, InterruptedException, ExecutionException {
+ Backup backup = client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)).build();
+ mockDatabaseAdmin.addFilterMatches("name:backups/" + BCK_ID, createBackupOperation.getName());
+ assertThat(backup.listBackupOperations().iterateAll()).hasSize(1);
+ client.createBackup(INSTANCE_ID, "other-backup", DB_ID, after7Days()).get();
+ assertThat(backup.listBackupOperations().iterateAll()).hasSize(1);
+ }
+
@Test
public void getAndSetIAMPolicy() {
Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID);
@@ -128,9 +723,41 @@ public void testDatabaseIAMPermissions() {
assertThat(permissions).containsExactly("spanner.databases.select");
}
+ private Timestamp after7Days() {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ + TimeUnit.MICROSECONDS.convert(7, TimeUnit.DAYS));
+ }
+
+ private Timestamp after14Days() {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ + TimeUnit.MICROSECONDS.convert(14, TimeUnit.DAYS));
+ }
+
private void createTestDatabase() {
try {
- client.createDatabase(INSTANCE_ID, DB_ID, INITIAL_STATEMENTS).get();
+ createDatabaseOperation = client.createDatabase(INSTANCE_ID, DB_ID, INITIAL_STATEMENTS);
+ createDatabaseOperation.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw SpannerExceptionFactory.newSpannerException(e);
+ }
+ }
+
+ private void createTestBackup() {
+ try {
+ createBackupOperation = client.createBackup(INSTANCE_ID, BCK_ID, DB_ID, after7Days());
+ createBackupOperation.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw SpannerExceptionFactory.newSpannerException(e);
+ }
+ }
+
+ private void restoreTestBackup() {
+ try {
+ restoreDatabaseOperation =
+ client.restoreDatabase(INSTANCE_ID, BCK_ID, INSTANCE_ID, RESTORED_ID);
+ restoreDatabaseOperation.get();
} catch (InterruptedException | ExecutionException e) {
throw SpannerExceptionFactory.newSpannerException(e);
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java
index c4e2e5316c..14b93d7413 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java
@@ -18,11 +18,13 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import com.google.cloud.Identity;
import com.google.cloud.Policy;
import com.google.cloud.Role;
+import com.google.cloud.Timestamp;
import com.google.cloud.spanner.DatabaseInfo.State;
import java.util.Arrays;
import org.junit.Before;
@@ -32,6 +34,9 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
/** Unit tests for {@link com.google.cloud.spanner.Database}. */
@RunWith(JUnit4.class)
@@ -45,6 +50,34 @@ public class DatabaseTest {
@Before
public void setUp() {
initMocks(this);
+ when(dbClient.newBackupBuilder(Mockito.any(BackupId.class)))
+ .thenAnswer(
+ new Answer() {
+ @Override
+ public Backup.Builder answer(InvocationOnMock invocation) throws Throwable {
+ return new Backup.Builder(dbClient, (BackupId) invocation.getArguments()[0]);
+ }
+ });
+ }
+
+ @Test
+ public void backup() {
+ Timestamp expireTime = Timestamp.now();
+ Database db = createDatabase();
+ db.backup(
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
+ .setExpireTime(expireTime)
+ .build());
+ verify(dbClient).createBackup("test-instance", "test-backup", "database-1", expireTime);
+ }
+
+ @Test
+ public void listDatabaseOperations() {
+ Database db = createDatabase();
+ db.listDatabaseOperations();
+ verify(dbClient)
+ .listDatabaseOperations("test-instance", Options.filter("name:databases/database-1"));
}
@Test
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java
index eaea833491..e5c0959b58 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java
@@ -51,7 +51,8 @@ public GceTestEnvConfig() {
double errorProbability =
Double.parseDouble(System.getProperty(GCE_STREAM_BROKEN_PROBABILITY, "0.0"));
checkState(errorProbability <= 1.0);
- SpannerOptions.Builder builder = SpannerOptions.newBuilder();
+ SpannerOptions.Builder builder =
+ SpannerOptions.newBuilder().setAutoThrottleAdministrativeRequests();
if (!projectId.isEmpty()) {
builder.setProjectId(projectId);
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceTest.java
index de0a3a564e..90f0b7630f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceTest.java
@@ -109,6 +109,38 @@ public void equality() {
tester.testEquals();
}
+ @Test
+ public void listDatabases() {
+ InstanceId id = new InstanceId("test-project", "test-instance");
+ Instance instance = new Instance.Builder(instanceClient, dbClient, id).build();
+ instance.listDatabases();
+ verify(dbClient).listDatabases("test-instance");
+ }
+
+ @Test
+ public void listBackups() {
+ InstanceId id = new InstanceId("test-project", "test-instance");
+ Instance instance = new Instance.Builder(instanceClient, dbClient, id).build();
+ instance.listBackups();
+ verify(dbClient).listBackups("test-instance");
+ }
+
+ @Test
+ public void listDatabaseOperations() {
+ InstanceId id = new InstanceId("test-project", "test-instance");
+ Instance instance = new Instance.Builder(instanceClient, dbClient, id).build();
+ instance.listDatabaseOperations();
+ verify(dbClient).listDatabaseOperations("test-instance");
+ }
+
+ @Test
+ public void listBackupOperations() {
+ InstanceId id = new InstanceId("test-project", "test-instance");
+ Instance instance = new Instance.Builder(instanceClient, dbClient, id).build();
+ instance.listBackupOperations();
+ verify(dbClient).listBackupOperations("test-instance");
+ }
+
@Test
public void getIAMPolicy() {
InstanceId id = new InstanceId("test-project", "test-instance");
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java
index de4681efa0..c198bc4f56 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import com.google.api.gax.grpc.testing.MockGrpcService;
+import com.google.common.base.Strings;
import com.google.iam.v1.GetIamPolicyRequest;
import com.google.iam.v1.Policy;
import com.google.iam.v1.SetIamPolicyRequest;
@@ -27,27 +28,51 @@
import com.google.protobuf.Any;
import com.google.protobuf.Empty;
import com.google.protobuf.Timestamp;
+import com.google.rpc.Code;
+import com.google.spanner.admin.database.v1.Backup;
+import com.google.spanner.admin.database.v1.BackupInfo;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
+import com.google.spanner.admin.database.v1.CreateBackupRequest;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseRequest;
import com.google.spanner.admin.database.v1.Database;
import com.google.spanner.admin.database.v1.Database.State;
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc.DatabaseAdminImplBase;
+import com.google.spanner.admin.database.v1.DeleteBackupRequest;
import com.google.spanner.admin.database.v1.DropDatabaseRequest;
+import com.google.spanner.admin.database.v1.GetBackupRequest;
import com.google.spanner.admin.database.v1.GetDatabaseDdlRequest;
import com.google.spanner.admin.database.v1.GetDatabaseDdlResponse;
import com.google.spanner.admin.database.v1.GetDatabaseRequest;
+import com.google.spanner.admin.database.v1.ListBackupOperationsRequest;
+import com.google.spanner.admin.database.v1.ListBackupOperationsResponse;
+import com.google.spanner.admin.database.v1.ListBackupsRequest;
+import com.google.spanner.admin.database.v1.ListBackupsResponse;
+import com.google.spanner.admin.database.v1.ListDatabaseOperationsRequest;
+import com.google.spanner.admin.database.v1.ListDatabaseOperationsResponse;
import com.google.spanner.admin.database.v1.ListDatabasesRequest;
import com.google.spanner.admin.database.v1.ListDatabasesResponse;
+import com.google.spanner.admin.database.v1.OperationProgress;
+import com.google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseRequest;
+import com.google.spanner.admin.database.v1.RestoreInfo;
+import com.google.spanner.admin.database.v1.RestoreSourceType;
+import com.google.spanner.admin.database.v1.UpdateBackupRequest;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest;
import io.grpc.ServerServiceDefinition;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Queue;
+import java.util.Random;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -57,16 +82,93 @@ public class MockDatabaseAdminServiceImpl extends DatabaseAdminImplBase implemen
private static final class MockDatabase {
private final String name;
private State state;
+ private final Timestamp createTime;
private final List ddl = new ArrayList<>();
+ private final RestoreInfo restoreInfo;
- private MockDatabase(String name, List ddl) {
+ private MockDatabase(String name, List ddl, RestoreInfo restoreInfo) {
this.name = name;
this.state = State.CREATING;
+ this.createTime =
+ Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1000L).build();
this.ddl.addAll(ddl);
+ this.restoreInfo = restoreInfo;
}
private Database toProto() {
- return Database.newBuilder().setName(name).setState(state).build();
+ return Database.newBuilder()
+ .setCreateTime(createTime)
+ .setName(name)
+ .setRestoreInfo(restoreInfo == null ? RestoreInfo.getDefaultInstance() : restoreInfo)
+ .setState(state)
+ .build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof MockDatabase)) {
+ return false;
+ }
+ return ((MockDatabase) o).name.equals(this.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+ }
+
+ static final class MockBackup {
+ private final String name;
+ private Backup.State state;
+ private final Timestamp createTime;
+ private final String database;
+ private final List ddl = new ArrayList<>();
+ private final List referencingDatabases = new ArrayList<>();
+ private final long size;
+ private Timestamp expireTime;
+
+ private MockBackup(String name, Backup backup, MockDatabase database) {
+ this.name = name;
+ this.state = Backup.State.CREATING;
+ this.createTime =
+ Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1000L).build();
+ this.database = database.name;
+ this.ddl.addAll(database.ddl);
+ this.size = RND.nextInt(Integer.MAX_VALUE);
+ this.expireTime = backup.getExpireTime();
+ }
+
+ private Backup toProto() {
+ return Backup.newBuilder()
+ .setCreateTime(createTime)
+ .setDatabase(database)
+ .setExpireTime(expireTime)
+ .setName(name)
+ .setSizeBytes(size)
+ .setState(state)
+ .addAllReferencingDatabases(referencingDatabases)
+ .build();
+ }
+
+ private BackupInfo toBackupInfo() {
+ return BackupInfo.newBuilder()
+ .setBackup(name)
+ .setCreateTime(createTime)
+ .setSourceDatabase(database)
+ .build();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDatabase() {
+ return database;
+ }
+
+ public Timestamp getExpireTime() {
+ return expireTime;
}
@Override
@@ -135,11 +237,205 @@ public Empty call() throws Exception {
}
}
+ private final class CreateBackupCallable implements Callable {
+ private final String operationName;
+ private final String name;
+
+ private CreateBackupCallable(String operationName, String name) {
+ this.operationName = operationName;
+ this.name = name;
+ }
+
+ @Override
+ public Backup call() throws Exception {
+ MockBackup backup = backups.get(name);
+ Backup proto = backup.toProto();
+ Operation operation = operations.get(operationName);
+ for (int progress = 1; progress <= 100; progress++) {
+ operation = operations.get(operationName);
+ long sleep = createBackupExecutionTime / 100;
+ if (progress == 100) {
+ sleep += createBackupExecutionTime % 100;
+ }
+ Thread.sleep(sleep);
+ if (operation != null) {
+ CreateBackupMetadata metadata =
+ operation.getMetadata().unpack(CreateBackupMetadata.class);
+ metadata =
+ metadata
+ .toBuilder()
+ .setProgress(
+ metadata.getProgress().toBuilder().setProgressPercent(progress).build())
+ .build();
+ operations.update(
+ operation
+ .toBuilder()
+ .setMetadata(Any.pack(metadata))
+ .setResponse(Any.pack(proto))
+ .build());
+ }
+ }
+ backup.state = Backup.State.READY;
+ proto = backup.toProto();
+ if (operation != null) {
+ CreateBackupMetadata metadata = operation.getMetadata().unpack(CreateBackupMetadata.class);
+ metadata =
+ metadata
+ .toBuilder()
+ .setProgress(
+ metadata
+ .getProgress()
+ .toBuilder()
+ .setProgressPercent(100)
+ .setEndTime(currentTime())
+ .build())
+ .build();
+ operations.update(
+ operation
+ .toBuilder()
+ .setDone(true)
+ .setMetadata(Any.pack(metadata))
+ .setResponse(Any.pack(proto))
+ .build());
+ }
+ return proto;
+ }
+ }
+
+ private final class RestoreDatabaseCallable implements Callable {
+ private final String operationName;
+ private final String name;
+
+ private RestoreDatabaseCallable(String operationName, String name) {
+ this.operationName = operationName;
+ this.name = name;
+ }
+
+ @Override
+ public Database call() throws Exception {
+ MockDatabase db = databases.get(name);
+ db.state = State.READY_OPTIMIZING;
+ Database proto = db.toProto();
+ Operation operation = operations.get(operationName);
+ for (int progress = 1; progress <= 100; progress++) {
+ long sleep = restoreDatabaseExecutionTime / 100;
+ if (progress == 100) {
+ sleep += restoreDatabaseExecutionTime % 100;
+ }
+ Thread.sleep(sleep);
+ if (operation != null) {
+ RestoreDatabaseMetadata metadata =
+ operation.getMetadata().unpack(RestoreDatabaseMetadata.class);
+ metadata =
+ metadata
+ .toBuilder()
+ .setProgress(
+ metadata.getProgress().toBuilder().setProgressPercent(progress).build())
+ .build();
+ operations.update(
+ operation
+ .toBuilder()
+ .setMetadata(Any.pack(metadata))
+ .setResponse(Any.pack(proto))
+ .build());
+ }
+ }
+ db.state = State.READY_OPTIMIZING;
+ proto = db.toProto();
+ if (operation != null) {
+ RestoreDatabaseMetadata metadata =
+ operation.getMetadata().unpack(RestoreDatabaseMetadata.class);
+ metadata =
+ metadata
+ .toBuilder()
+ .setProgress(
+ metadata
+ .getProgress()
+ .toBuilder()
+ .setEndTime(currentTime())
+ .setProgressPercent(100)
+ .build())
+ .build();
+ operations.update(
+ operation
+ .toBuilder()
+ .setDone(true)
+ .setMetadata(Any.pack(metadata))
+ .setResponse(Any.pack(proto))
+ .build());
+ }
+ return proto;
+ }
+ }
+
+ private final class OptimizeDatabaseCallable implements Callable {
+ private final String operationName;
+ private final String restoreOperationName;
+ private final String name;
+
+ private OptimizeDatabaseCallable(
+ String operationName, String restoreOperationName, String name) {
+ this.operationName = operationName;
+ this.restoreOperationName = restoreOperationName;
+ this.name = name;
+ }
+
+ @Override
+ public Database call() throws Exception {
+ MockDatabase db = databases.get(name);
+ Operation operation = operations.get(operationName);
+ try {
+ // Wait until the restore operation has finished.
+ Operation restoreOperation = operations.get(restoreOperationName);
+ while (!restoreOperation.getDone()) {
+ Thread.sleep(10L);
+ restoreOperation = operations.get(restoreOperationName);
+ }
+ Thread.sleep(optimizeDatabaseExecutionTime);
+ db.state = State.READY;
+ Database proto = db.toProto();
+ if (operation != null) {
+ operations.update(
+ operation.toBuilder().setDone(true).setResponse(Any.pack(proto)).build());
+ }
+ return proto;
+ } catch (Exception e) {
+ if (operation != null) {
+ Database proto = db.toProto();
+ operations.update(
+ operation
+ .toBuilder()
+ .setDone(true)
+ .setError(fromException(e))
+ .setResponse(Any.pack(proto))
+ .build());
+ }
+ throw e;
+ }
+ }
+ }
+
+ private com.google.rpc.Status fromException(Exception e) {
+ int code = Code.UNKNOWN_VALUE;
+ if (e instanceof InterruptedException) {
+ code = Code.CANCELLED_VALUE;
+ }
+ return com.google.rpc.Status.newBuilder().setCode(code).setMessage(e.getMessage()).build();
+ }
+
private ConcurrentMap policies = new ConcurrentHashMap<>();
+ private static final String EXPIRE_TIME_MASK = "expire_time";
+ private static final Random RND = new Random();
private final Queue exceptions = new ConcurrentLinkedQueue<>();
private final ConcurrentMap databases = new ConcurrentHashMap<>();
+ private final ConcurrentMap backups = new ConcurrentHashMap<>();
+ private final ConcurrentMap> filterMatches = new ConcurrentHashMap<>();
private final MockOperationsServiceImpl operations;
+ private long createBackupExecutionTime;
+ private long restoreDatabaseExecutionTime;
+ private long optimizeDatabaseExecutionTime;
+
public MockDatabaseAdminServiceImpl(MockOperationsServiceImpl operations) {
this.operations = operations;
}
@@ -152,7 +448,7 @@ public void createDatabase(
id = id.substring(1, id.length() - 1);
}
String name = String.format("%s/databases/%s", request.getParent(), id);
- MockDatabase db = new MockDatabase(name, request.getExtraStatementsList());
+ MockDatabase db = new MockDatabase(name, request.getExtraStatementsList(), null);
if (databases.putIfAbsent(name, db) == null) {
CreateDatabaseMetadata metadata =
CreateDatabaseMetadata.newBuilder().setDatabase(name).build();
@@ -191,7 +487,11 @@ public void getDatabase(GetDatabaseRequest request, StreamObserver res
MockDatabase db = databases.get(request.getName());
if (db != null) {
responseObserver.onNext(
- Database.newBuilder().setName(request.getName()).setState(State.READY).build());
+ Database.newBuilder()
+ .setName(request.getName())
+ .setCreateTime(db.createTime)
+ .setState(State.READY)
+ .build());
responseObserver.onCompleted();
} else {
responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
@@ -215,12 +515,50 @@ public void listDatabases(
ListDatabasesRequest request, StreamObserver responseObserver) {
List dbs = new ArrayList<>(databases.size());
for (Entry entry : databases.entrySet()) {
- dbs.add(Database.newBuilder().setName(entry.getKey()).setState(State.READY).build());
+ dbs.add(
+ Database.newBuilder()
+ .setName(entry.getKey())
+ .setCreateTime(entry.getValue().createTime)
+ .setState(State.READY)
+ .build());
}
responseObserver.onNext(ListDatabasesResponse.newBuilder().addAllDatabases(dbs).build());
responseObserver.onCompleted();
}
+ @Override
+ public void listDatabaseOperations(
+ ListDatabaseOperationsRequest request,
+ StreamObserver responseObserver) {
+ ListDatabaseOperationsResponse.Builder builder = ListDatabaseOperationsResponse.newBuilder();
+ try {
+ for (Operation op : operations.iterable()) {
+ if (op.getName().matches(".*?/databases\\/.*?/operations/.*?")
+ && op.getName().startsWith(request.getParent())) {
+ if (matchesFilter(op, request.getFilter())) {
+ builder.addOperations(op);
+ }
+ }
+ }
+ responseObserver.onNext(builder.build());
+ responseObserver.onCompleted();
+ } catch (Exception e) {
+ responseObserver.onError(e);
+ }
+ }
+
+ private boolean matchesFilter(Object obj, String filter) throws Exception {
+ if (!Strings.isNullOrEmpty(filter)) {
+ Set matches = filterMatches.get(filter);
+ if (matches != null) {
+ String name = (String) obj.getClass().getMethod("getName").invoke(obj);
+ return matches.contains(name);
+ }
+ return false;
+ }
+ return true;
+ }
+
@Override
public void updateDatabaseDdl(
UpdateDatabaseDdlRequest request, StreamObserver responseObserver) {
@@ -247,6 +585,214 @@ public void updateDatabaseDdl(
}
}
+ @Override
+ public void createBackup(
+ CreateBackupRequest request, StreamObserver responseObserver) {
+ String name = String.format("%s/backups/%s", request.getParent(), request.getBackupId());
+ MockDatabase db = databases.get(request.getBackup().getDatabase());
+ if (db == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription(
+ String.format(
+ "Database with name %s not found", request.getBackup().getDatabase()))
+ .asRuntimeException());
+ return;
+ }
+ MockBackup bck = new MockBackup(name, request.getBackup(), db);
+ if (backups.putIfAbsent(name, bck) == null) {
+ CreateBackupMetadata metadata =
+ CreateBackupMetadata.newBuilder()
+ .setName(name)
+ .setDatabase(bck.database)
+ .setProgress(
+ OperationProgress.newBuilder()
+ .setStartTime(
+ Timestamp.newBuilder()
+ .setSeconds(System.currentTimeMillis() / 1000L)
+ .build())
+ .setProgressPercent(0))
+ .build();
+ Operation operation =
+ Operation.newBuilder()
+ .setMetadata(Any.pack(metadata))
+ .setResponse(Any.pack(bck.toProto()))
+ .setName(operations.generateOperationName(name))
+ .build();
+ operations.addOperation(operation, new CreateBackupCallable(operation.getName(), name));
+ responseObserver.onNext(operation);
+ responseObserver.onCompleted();
+ } else {
+ responseObserver.onError(
+ Status.ALREADY_EXISTS
+ .withDescription(String.format("Backup with name %s already exists", name))
+ .asRuntimeException());
+ }
+ }
+
+ @Override
+ public void deleteBackup(DeleteBackupRequest request, StreamObserver responseObserver) {
+ MockBackup bck = backups.get(request.getName());
+ if (backups.remove(request.getName(), bck)) {
+ responseObserver.onNext(Empty.getDefaultInstance());
+ responseObserver.onCompleted();
+ } else {
+ responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
+ }
+ }
+
+ @Override
+ public void getBackup(GetBackupRequest request, StreamObserver responseObserver) {
+ MockBackup bck = backups.get(request.getName());
+ if (bck != null) {
+ responseObserver.onNext(
+ Backup.newBuilder()
+ .setName(request.getName())
+ .setCreateTime(bck.createTime)
+ .setDatabase(bck.database)
+ .setExpireTime(bck.expireTime)
+ .setSizeBytes(bck.size)
+ .setState(Backup.State.READY)
+ .build());
+ responseObserver.onCompleted();
+ } else {
+ responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
+ }
+ }
+
+ @Override
+ public void listBackups(
+ ListBackupsRequest request, StreamObserver responseObserver) {
+ List bcks = new ArrayList<>(backups.size());
+ try {
+ for (Entry entry : backups.entrySet()) {
+ if (matchesFilter(entry.getValue(), request.getFilter())) {
+ bcks.add(
+ Backup.newBuilder()
+ .setName(entry.getKey())
+ .setCreateTime(entry.getValue().createTime)
+ .setDatabase(entry.getValue().database)
+ .setExpireTime(entry.getValue().expireTime)
+ .setSizeBytes(entry.getValue().size)
+ .setState(Backup.State.READY)
+ .build());
+ }
+ }
+ responseObserver.onNext(ListBackupsResponse.newBuilder().addAllBackups(bcks).build());
+ responseObserver.onCompleted();
+ } catch (Exception e) {
+ responseObserver.onError(e);
+ }
+ }
+
+ @Override
+ public void listBackupOperations(
+ ListBackupOperationsRequest request,
+ StreamObserver responseObserver) {
+ ListBackupOperationsResponse.Builder builder = ListBackupOperationsResponse.newBuilder();
+ try {
+ for (Operation op : operations.iterable()) {
+ if (op.getName().matches(".*?/backups/.*?/operations/.*?")
+ && op.getName().startsWith(request.getParent())) {
+ if (matchesFilter(op, request.getFilter())) {
+ builder.addOperations(op);
+ }
+ }
+ }
+ responseObserver.onNext(builder.build());
+ responseObserver.onCompleted();
+ } catch (Exception e) {
+ responseObserver.onError(e);
+ }
+ }
+
+ @Override
+ public void updateBackup(UpdateBackupRequest request, StreamObserver responseObserver) {
+ MockBackup bck = backups.get(request.getBackup().getName());
+ if (bck != null) {
+ if (request.getUpdateMask().getPathsList().contains(EXPIRE_TIME_MASK)) {
+ bck.expireTime = request.getBackup().getExpireTime();
+ }
+ responseObserver.onNext(
+ Backup.newBuilder()
+ .setName(bck.name)
+ .setCreateTime(bck.createTime)
+ .setDatabase(bck.database)
+ .setExpireTime(bck.expireTime)
+ .setSizeBytes(bck.size)
+ .setState(Backup.State.READY)
+ .build());
+ responseObserver.onCompleted();
+ } else {
+ responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
+ }
+ }
+
+ @Override
+ public void restoreDatabase(
+ RestoreDatabaseRequest request, StreamObserver responseObserver) {
+ MockBackup bck = backups.get(request.getBackup());
+ if (bck != null) {
+ String name = String.format("%s/databases/%s", request.getParent(), request.getDatabaseId());
+ MockDatabase db =
+ new MockDatabase(
+ name,
+ bck.ddl,
+ RestoreInfo.newBuilder()
+ .setBackupInfo(bck.toBackupInfo())
+ .setSourceType(RestoreSourceType.BACKUP)
+ .build());
+ if (databases.putIfAbsent(name, db) == null) {
+ bck.referencingDatabases.add(db.name);
+ Operation optimizeOperation =
+ Operation.newBuilder()
+ .setDone(false)
+ .setName(operations.generateOperationName(name))
+ .setMetadata(
+ Any.pack(
+ OptimizeRestoredDatabaseMetadata.newBuilder()
+ .setName(name)
+ .setProgress(
+ OperationProgress.newBuilder()
+ .setStartTime(currentTime())
+ .setProgressPercent(0)
+ .build())
+ .build()))
+ .setResponse(Any.pack(db.toProto()))
+ .build();
+ RestoreDatabaseMetadata metadata =
+ RestoreDatabaseMetadata.newBuilder()
+ .setBackupInfo(bck.toBackupInfo())
+ .setName(name)
+ .setProgress(
+ OperationProgress.newBuilder()
+ .setStartTime(currentTime())
+ .setProgressPercent(0)
+ .build())
+ .setOptimizeDatabaseOperationName(optimizeOperation.getName())
+ .setSourceType(RestoreSourceType.BACKUP)
+ .build();
+ Operation operation =
+ Operation.newBuilder()
+ .setMetadata(Any.pack(metadata))
+ .setResponse(Any.pack(db.toProto()))
+ .setDone(false)
+ .setName(operations.generateOperationName(name))
+ .build();
+ operations.addOperation(operation, new RestoreDatabaseCallable(operation.getName(), name));
+ operations.addOperation(
+ optimizeOperation,
+ new OptimizeDatabaseCallable(optimizeOperation.getName(), operation.getName(), name));
+ responseObserver.onNext(operation);
+ responseObserver.onCompleted();
+ } else {
+ responseObserver.onError(Status.ALREADY_EXISTS.asRuntimeException());
+ }
+ } else {
+ responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
+ }
+ }
+
@Override
public void getIamPolicy(GetIamPolicyRequest request, StreamObserver responseObserver) {
Policy policy = policies.get(request.getResource());
@@ -292,6 +838,19 @@ public void addException(Exception exception) {
exceptions.add(exception);
}
+ public void addFilterMatches(String filter, String... names) {
+ Set matches = filterMatches.get(filter);
+ if (matches == null) {
+ matches = new HashSet<>();
+ filterMatches.put(filter, matches);
+ }
+ matches.addAll(Arrays.asList(names));
+ }
+
+ public void clearFilterMatches() {
+ filterMatches.clear();
+ }
+
@Override
public ServerServiceDefinition getServiceDefinition() {
return bindService();
@@ -302,6 +861,8 @@ public void reset() {
exceptions.clear();
policies.clear();
databases.clear();
+ backups.clear();
+ filterMatches.clear();
}
private Timestamp currentTime() {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java
index b143418859..4364bfe26a 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java
@@ -32,15 +32,30 @@
import com.google.api.gax.rpc.StatusCode.Code;
import com.google.cloud.spanner.OperationFutureUtil.FakeStatusCode;
import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient;
+import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListBackupOperationsPagedResponse;
+import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListBackupsPagedResponse;
+import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListDatabaseOperationsPagedResponse;
import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListDatabasesPagedResponse;
import com.google.cloud.spanner.admin.database.v1.DatabaseAdminSettings;
+import com.google.iam.v1.Binding;
+import com.google.iam.v1.GetIamPolicyRequest;
+import com.google.iam.v1.Policy;
+import com.google.iam.v1.SetIamPolicyRequest;
import com.google.iam.v1.TestIamPermissionsRequest;
import com.google.iam.v1.TestIamPermissionsResponse;
+import com.google.longrunning.Operation;
import com.google.protobuf.Empty;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.Timestamp;
+import com.google.spanner.admin.database.v1.Backup;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
+import com.google.spanner.admin.database.v1.CreateBackupRequest;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseRequest;
import com.google.spanner.admin.database.v1.Database;
import com.google.spanner.admin.database.v1.GetDatabaseDdlResponse;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseRequest;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest;
import java.io.IOException;
@@ -48,6 +63,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.After;
@@ -101,6 +117,7 @@ public void describeTo(Description description) {
private static final String TEST_PARENT = "projects/my-project/instances/my-instance";
private static final String TEST_DB_NAME = String.format("%s/databases/test-db", TEST_PARENT);
+ private static final String TEST_BCK_NAME = String.format("%s/backups/test-bck", TEST_PARENT);
private static MockOperationsServiceImpl mockOperations;
private static MockDatabaseAdminServiceImpl mockDatabaseAdmin;
private static MockServiceHelper serviceHelper;
@@ -130,6 +147,20 @@ public void setUp() throws IOException {
DatabaseAdminSettings.newBuilder()
.setTransportChannelProvider(channelProvider)
.setCredentialsProvider(NoCredentialsProvider.create());
+ settingsBuilder
+ .createBackupOperationSettings()
+ .setPollingAlgorithm(
+ OperationTimedPollAlgorithm.create(
+ RetrySettings.newBuilder()
+ .setInitialRpcTimeout(Duration.ofMillis(20L))
+ .setInitialRetryDelay(Duration.ofMillis(10L))
+ .setMaxRetryDelay(Duration.ofMillis(150L))
+ .setMaxRpcTimeout(Duration.ofMillis(150L))
+ .setMaxAttempts(10)
+ .setTotalTimeout(Duration.ofMillis(5000L))
+ .setRetryDelayMultiplier(1.3)
+ .setRpcTimeoutMultiplier(1.3)
+ .build()));
settingsBuilder
.createDatabaseOperationSettings()
.setPollingAlgorithm(
@@ -144,6 +175,20 @@ public void setUp() throws IOException {
.setRetryDelayMultiplier(1.3)
.setRpcTimeoutMultiplier(1.3)
.build()));
+ settingsBuilder
+ .restoreDatabaseOperationSettings()
+ .setPollingAlgorithm(
+ OperationTimedPollAlgorithm.create(
+ RetrySettings.newBuilder()
+ .setInitialRpcTimeout(Duration.ofMillis(20L))
+ .setInitialRetryDelay(Duration.ofMillis(10L))
+ .setMaxRetryDelay(Duration.ofMillis(150L))
+ .setMaxRpcTimeout(Duration.ofMillis(150L))
+ .setMaxAttempts(10)
+ .setTotalTimeout(Duration.ofMillis(5000L))
+ .setRetryDelayMultiplier(1.3)
+ .setRpcTimeoutMultiplier(1.3)
+ .build()));
client = DatabaseAdminClient.create(settingsBuilder.build());
}
@@ -239,6 +284,17 @@ public void listDatabases() {
assertThat(databases).containsExactly(TEST_DB_NAME);
}
+ @Test
+ public void listDatabaseOperations() {
+ createTestDb();
+ ListDatabaseOperationsPagedResponse response = client.listDatabaseOperations(TEST_DB_NAME);
+ List operations = new ArrayList<>();
+ for (Operation op : response.iterateAll()) {
+ operations.add(op);
+ }
+ assertThat(operations).hasSize(1);
+ }
+
@Test
public void updateDatabaseDdl() throws InterruptedException, ExecutionException {
createTestDb();
@@ -256,6 +312,174 @@ public void updateDatabaseDdl() throws InterruptedException, ExecutionException
"CREATE TABLE FOO", "CREATE TABLE BAR", "CREATE TABLE BAZ", "DROP TABLE FOO");
}
+ private Backup createTestBackup() {
+ CreateBackupRequest request =
+ CreateBackupRequest.newBuilder()
+ .setBackupId("test-bck")
+ .setBackup(
+ Backup.newBuilder()
+ .setDatabase(TEST_DB_NAME)
+ .setExpireTime(
+ Timestamp.newBuilder()
+ .setSeconds(
+ System.currentTimeMillis() * 1000L
+ + TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)))
+ .build())
+ .setParent(TEST_PARENT)
+ .build();
+ OperationFuture op =
+ client.createBackupOperationCallable().futureCall(request);
+ try {
+ return op.get();
+ } catch (ExecutionException e) {
+ if (e.getCause() != null && e.getCause() instanceof RuntimeException) {
+ throw (RuntimeException) e.getCause();
+ }
+ throw new RuntimeException(e);
+ } catch (InterruptedException e) {
+ throw new CancelledException(e, FakeStatusCode.of(Code.CANCELLED), false);
+ }
+ }
+
+ @Test
+ public void createBackup() {
+ createTestDb();
+ Backup bck = createTestBackup();
+ assertThat(bck.getName()).isEqualTo(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void createBackupAlreadyExists() {
+ createTestDb();
+ createTestBackup();
+ exception.expect(ApiExceptionMatcher.forCode(StatusCode.Code.ALREADY_EXISTS));
+ createTestBackup();
+ }
+
+ @Test
+ public void createBackupDatabaseDoesNotExist() {
+ exception.expect(ApiExceptionMatcher.forCode(StatusCode.Code.NOT_FOUND));
+ createTestBackup();
+ }
+
+ @Test
+ public void deleteBackup() {
+ createTestDb();
+ createTestBackup();
+ Backup bck = client.getBackup(TEST_BCK_NAME);
+ assertThat(bck.getName()).isEqualTo(TEST_BCK_NAME);
+ client.deleteBackup(TEST_BCK_NAME);
+ exception.expect(ApiExceptionMatcher.forCode(StatusCode.Code.NOT_FOUND));
+ client.getBackup(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void deleteBackupNotFound() {
+ exception.expect(ApiExceptionMatcher.forCode(StatusCode.Code.NOT_FOUND));
+ client.deleteBackup(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void getBackup() {
+ createTestDb();
+ createTestBackup();
+ Backup bck = client.getBackup(TEST_BCK_NAME);
+ assertThat(bck.getName()).isEqualTo(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void getBackupNotFound() {
+ exception.expect(ApiExceptionMatcher.forCode(StatusCode.Code.NOT_FOUND));
+ client.getBackup(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void listBackups() {
+ createTestDb();
+ createTestBackup();
+ ListBackupsPagedResponse response = client.listBackups(TEST_PARENT);
+ List backups = new ArrayList<>();
+ for (Backup bck : response.iterateAll()) {
+ backups.add(bck.getName());
+ }
+ assertThat(backups).containsExactly(TEST_BCK_NAME);
+ }
+
+ @Test
+ public void listBackupOperations() {
+ createTestDb();
+ createTestBackup();
+ ListBackupOperationsPagedResponse response = client.listBackupOperations(TEST_BCK_NAME);
+ List operations = new ArrayList<>();
+ for (Operation op : response.iterateAll()) {
+ operations.add(op);
+ }
+ assertThat(operations).hasSize(1);
+ }
+
+ @Test
+ public void updateBackup() {
+ createTestDb();
+ Backup backup = createTestBackup();
+ Backup toBeUpdated =
+ backup.toBuilder().setExpireTime(Timestamp.newBuilder().setSeconds(1000L).build()).build();
+ Backup updated =
+ client.updateBackup(toBeUpdated, FieldMask.newBuilder().addPaths("expire_time").build());
+ assertThat(updated.getExpireTime()).isEqualTo(toBeUpdated.getExpireTime());
+ assertThat(backup.getExpireTime()).isNotEqualTo(updated.getExpireTime());
+ }
+
+ @Test
+ public void restoreDatabase() throws InterruptedException, ExecutionException {
+ createTestDb();
+ createTestBackup();
+ RestoreDatabaseRequest request =
+ RestoreDatabaseRequest.newBuilder()
+ .setBackup(TEST_BCK_NAME)
+ .setDatabaseId("restored-db")
+ .setParent(TEST_PARENT)
+ .build();
+ OperationFuture op =
+ client.restoreDatabaseOperationCallable().futureCall(request);
+ Database restoredDb = op.get();
+ assertThat(restoredDb.getName())
+ .isEqualTo(String.format("%s/databases/%s", TEST_PARENT, "restored-db"));
+ assertThat(restoredDb.getRestoreInfo().getBackupInfo().getBackup()).isEqualTo(TEST_BCK_NAME);
+ assertThat(restoredDb.getRestoreInfo().getBackupInfo().getSourceDatabase())
+ .isEqualTo(TEST_DB_NAME);
+ }
+
+ @Test
+ public void restoreDatabaseNotFound() throws InterruptedException, ExecutionException {
+ createTestDb();
+ RestoreDatabaseRequest request =
+ RestoreDatabaseRequest.newBuilder()
+ .setBackup(TEST_BCK_NAME)
+ .setDatabaseId("restored-db")
+ .setParent(TEST_PARENT)
+ .build();
+ OperationFuture op =
+ client.restoreDatabaseOperationCallable().futureCall(request);
+ exception.expect(ApiExceptionMatcher.forCode(StatusCode.Code.NOT_FOUND));
+ op.get();
+ }
+
+ @Test
+ public void restoreDatabaseAlreadyExists() throws InterruptedException, ExecutionException {
+ createTestDb();
+ createTestBackup();
+ RestoreDatabaseRequest request =
+ RestoreDatabaseRequest.newBuilder()
+ .setBackup(TEST_BCK_NAME)
+ .setDatabaseId("test-db")
+ .setParent(TEST_PARENT)
+ .build();
+ OperationFuture op =
+ client.restoreDatabaseOperationCallable().futureCall(request);
+ exception.expect(ApiExceptionMatcher.forCode(StatusCode.Code.ALREADY_EXISTS));
+ op.get();
+ }
+
@Test
public void testIAMPolicy() {
TestIamPermissionsResponse response =
@@ -267,5 +491,30 @@ public void testIAMPolicy() {
.build());
assertThat(response.getPermissionsList())
.containsExactly("spanner.databases.select", "spanner.databases.write");
+
+ GetIamPolicyRequest request = GetIamPolicyRequest.newBuilder().setResource(TEST_PARENT).build();
+ Policy policy = client.getIamPolicy(request);
+ assertThat(policy).isNotNull();
+
+ Policy newPolicy =
+ Policy.newBuilder()
+ .addBindings(
+ Binding.newBuilder().setRole("roles/admin").addMembers("user:joe@example.com"))
+ .setEtag(policy.getEtag())
+ .build();
+ client.setIamPolicy(
+ SetIamPolicyRequest.newBuilder().setResource(TEST_PARENT).setPolicy(newPolicy).build());
+ policy = client.getIamPolicy(TEST_PARENT);
+ assertThat(policy).isEqualTo(newPolicy);
+
+ response =
+ client.testIamPermissions(
+ TestIamPermissionsRequest.newBuilder()
+ .setResource(TEST_PARENT)
+ .addPermissions("spanner.databases.select")
+ .addPermissions("spanner.databases.update")
+ .build());
+ assertThat(response.getPermissionsList())
+ .containsExactly("spanner.databases.select", "spanner.databases.update");
}
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockOperationsServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockOperationsServiceImpl.java
index b809358740..35d6bcdce3 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockOperationsServiceImpl.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockOperationsServiceImpl.java
@@ -70,7 +70,10 @@ Operation get(String name) {
}
void update(Operation operation) {
- operations.put(operation.getName(), operation);
+ Operation existing = operations.get(operation.getName());
+ if (!existing.getDone()) {
+ operations.put(operation.getName(), operation);
+ }
}
Iterable iterable() {
@@ -133,8 +136,21 @@ public void cancelOperation(
Future> fut = futures.get(request.getName());
if (op != null && fut != null) {
if (!op.getDone()) {
+ operations.put(
+ request.getName(),
+ op.toBuilder()
+ .clearResponse()
+ .setDone(true)
+ .setError(
+ com.google.rpc.Status.newBuilder()
+ .setCode(Status.CANCELLED.getCode().value())
+ .setMessage("Operation was cancelled")
+ .build())
+ .build());
fut.cancel(true);
}
+ responseObserver.onNext(Empty.getDefaultInstance());
+ responseObserver.onCompleted();
} else {
responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ParallelIntegrationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ParallelIntegrationTest.java
new file mode 100644
index 0000000000..382f826c6a
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ParallelIntegrationTest.java
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+/** Parallel Integration Test interface. */
+public interface ParallelIntegrationTest {}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
new file mode 100644
index 0000000000..f0f8434ed6
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
@@ -0,0 +1,559 @@
+/*
+ * 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.it;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.api.gax.longrunning.OperationFuture;
+import com.google.api.gax.paging.Page;
+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.ErrorCode;
+import com.google.cloud.spanner.Instance;
+import com.google.cloud.spanner.InstanceAdminClient;
+import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.Options;
+import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.longrunning.Operation;
+import com.google.spanner.admin.database.v1.CreateBackupMetadata;
+import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreSourceType;
+import io.grpc.Status;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Integration tests creating, reading, updating and deleting backups. This test class combines
+ * several tests into one long test to reduce the total execution time.
+ */
+@Category(ParallelIntegrationTest.class)
+@RunWith(JUnit4.class)
+public class ITBackupTest {
+ private static final Logger logger = Logger.getLogger(ITBackupTest.class.getName());
+ private static final String EXPECTED_OP_NAME_FORMAT = "%s/backups/%s/operations/";
+ @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+
+ @Rule public ExpectedException expectedException = ExpectedException.none();
+ private DatabaseAdminClient dbAdminClient;
+ private InstanceAdminClient instanceAdminClient;
+ private Instance instance;
+ private RemoteSpannerHelper testHelper;
+ private final AtomicInteger backupSeq = new AtomicInteger();
+ private List databases = new ArrayList<>();
+ private List backups = new ArrayList<>();
+ private final Random random = new Random();
+
+ @Before
+ public void setUp() throws Exception {
+ logger.info("Setting up tests");
+ testHelper = env.getTestHelper();
+ dbAdminClient = testHelper.getClient().getDatabaseAdminClient();
+ instanceAdminClient = testHelper.getClient().getInstanceAdminClient();
+ instance = instanceAdminClient.getInstance(testHelper.getInstanceId().getInstance());
+ logger.info("Finished setup");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (String backup : backups) {
+ waitForDbOperations(backup);
+ dbAdminClient.deleteBackup(testHelper.getInstanceId().getInstance(), backup);
+ }
+ backups.clear();
+ for (String db : databases) {
+ dbAdminClient.dropDatabase(testHelper.getInstanceId().getInstance(), db);
+ }
+ }
+
+ private void waitForDbOperations(String backupId) throws InterruptedException {
+ try {
+ Backup backupMetadata =
+ dbAdminClient.getBackup(testHelper.getInstanceId().getInstance(), backupId);
+ boolean allDbOpsDone = false;
+ while (!allDbOpsDone) {
+ allDbOpsDone = true;
+ for (String referencingDb : backupMetadata.getProto().getReferencingDatabasesList()) {
+ String filter =
+ String.format(
+ "name:%s/operations/ AND "
+ + "(metadata.@type:type.googleapis.com/"
+ + "google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)",
+ referencingDb);
+ for (Operation op :
+ dbAdminClient
+ .listDatabaseOperations(
+ testHelper.getInstanceId().getInstance(), Options.filter(filter))
+ .iterateAll()) {
+ if (!op.getDone()) {
+ Thread.sleep(5000L);
+ allDbOpsDone = false;
+ break;
+ }
+ }
+ }
+ }
+ } catch (SpannerException e) {
+ if (e.getErrorCode() == ErrorCode.NOT_FOUND) {
+ return;
+ }
+ throw e;
+ }
+ }
+
+ private String getUniqueBackupId() {
+ return String.format("testbck_%06d_%04d", random.nextInt(1000000), backupSeq.incrementAndGet());
+ }
+
+ private static Timestamp after7Days() {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ + TimeUnit.MICROSECONDS.convert(7L, TimeUnit.DAYS));
+ }
+
+ private Timestamp after5Minutes() {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ + TimeUnit.MICROSECONDS.convert(5L, TimeUnit.MINUTES));
+ }
+
+ private Timestamp tomorrow() {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ + TimeUnit.MICROSECONDS.convert(1L, TimeUnit.DAYS));
+ }
+
+ private Timestamp yesterday() {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ - TimeUnit.MICROSECONDS.convert(1L, TimeUnit.DAYS));
+ }
+
+ @Test
+ public void testBackups() throws InterruptedException, ExecutionException {
+ // Create two test databases in parallel.
+ String db1Id = testHelper.getUniqueDatabaseId() + "_db1";
+ logger.info(String.format("Creating test database %s", db1Id));
+ OperationFuture dbOp1 =
+ dbAdminClient.createDatabase(
+ testHelper.getInstanceId().getInstance(),
+ db1Id,
+ Arrays.asList("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)"));
+ String db2Id = testHelper.getUniqueDatabaseId() + "_db2";
+ logger.info(String.format("Creating test database %s", db2Id));
+ OperationFuture dbOp2 =
+ dbAdminClient.createDatabase(
+ testHelper.getInstanceId().getInstance(),
+ testHelper.getUniqueDatabaseId() + "_db2",
+ Arrays.asList("CREATE TABLE BAR (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)"));
+ // Make sure all databases are created before we try to create any backups.
+ Database db1 = dbOp1.get();
+ Database db2 = dbOp2.get();
+ databases.add(db1.getId().getDatabase());
+ databases.add(db2.getId().getDatabase());
+ // Insert some data into db2 to make sure the backup will have a size>0.
+ DatabaseClient client = testHelper.getDatabaseClient(db2);
+ client.writeAtLeastOnce(
+ Arrays.asList(
+ Mutation.newInsertOrUpdateBuilder("BAR")
+ .set("ID")
+ .to(1L)
+ .set("NAME")
+ .to("TEST")
+ .build()));
+
+ // Create two backups in parallel.
+ String backupId1 = getUniqueBackupId() + "_bck1";
+ String backupId2 = getUniqueBackupId() + "_bck2";
+ Timestamp expireTime = after7Days();
+ logger.info(String.format("Creating backups %s and %s in parallel", backupId1, backupId2));
+ OperationFuture op1 =
+ dbAdminClient.createBackup(
+ testHelper.getInstanceId().getInstance(),
+ backupId1,
+ db1.getId().getDatabase(),
+ expireTime);
+ OperationFuture op2 =
+ dbAdminClient.createBackup(
+ testHelper.getInstanceId().getInstance(),
+ backupId2,
+ db2.getId().getDatabase(),
+ expireTime);
+ backups.add(backupId1);
+ backups.add(backupId2);
+
+ // Execute metadata tests as part of this integration test to reduce total execution time.
+ testMetadata(op1, op2, backupId1, backupId2, db1, db2);
+
+ // Ensure both backups have been created before we proceed.
+ Backup backup1 = op1.get();
+ Backup backup2 = op2.get();
+ // Insert some more data into db2 to get a timestamp from the server.
+ Timestamp commitTs =
+ client.writeAtLeastOnce(
+ Arrays.asList(
+ Mutation.newInsertOrUpdateBuilder("BAR")
+ .set("ID")
+ .to(2L)
+ .set("NAME")
+ .to("TEST2")
+ .build()));
+
+ // Test listing operations.
+ // List all backups.
+ logger.info("Listing all backups");
+ assertThat(instance.listBackups().iterateAll()).containsAtLeast(backup1, backup2);
+ // List all backups whose names contain 'bck1'.
+ logger.info("Listing backups with name bck1");
+ assertThat(
+ dbAdminClient
+ .listBackups(
+ testHelper.getInstanceId().getInstance(),
+ Options.filter(String.format("name:%s", backup1.getId().getName())))
+ .iterateAll())
+ .containsExactly(backup1);
+ logger.info("Listing ready backups");
+ Iterable readyBackups =
+ dbAdminClient
+ .listBackups(testHelper.getInstanceId().getInstance(), Options.filter("state:READY"))
+ .iterateAll();
+ assertThat(readyBackups).containsAtLeast(backup1, backup2);
+ // List all backups for databases whose names contain 'db1'.
+ logger.info("Listing backups for database db1");
+ assertThat(
+ dbAdminClient
+ .listBackups(
+ testHelper.getInstanceId().getInstance(),
+ Options.filter(String.format("database:%s", db1.getId().getName())))
+ .iterateAll())
+ .containsExactly(backup1);
+ // List all backups that were created before a certain time.
+ Timestamp ts = Timestamp.ofTimeSecondsAndNanos(commitTs.getSeconds(), 0);
+ logger.info(String.format("Listing backups created before %s", ts));
+ assertThat(
+ dbAdminClient
+ .listBackups(
+ testHelper.getInstanceId().getInstance(),
+ Options.filter(String.format("create_time<\"%s\"", ts)))
+ .iterateAll())
+ .containsAtLeast(backup1, backup2);
+ // List all backups with a size > 0.
+ logger.info("Listing backups with size>0");
+ assertThat(
+ dbAdminClient
+ .listBackups(
+ testHelper.getInstanceId().getInstance(), Options.filter("size_bytes>0"))
+ .iterateAll())
+ .contains(backup2);
+ assertThat(
+ dbAdminClient
+ .listBackups(
+ testHelper.getInstanceId().getInstance(), Options.filter("size_bytes>0"))
+ .iterateAll())
+ .doesNotContain(backup1);
+
+ // Test pagination.
+ testPagination(2);
+ logger.info("Finished listBackup tests");
+
+ // Execute other tests as part of this integration test to reduce total execution time.
+ testGetBackup(db2, backupId2, expireTime);
+ testUpdateBackup(backup1);
+ testCreateInvalidExpirationDate(db1);
+ testRestore(backup1, op1);
+
+ testDelete(backupId2);
+ testCancelBackupOperation(db1);
+ // Finished all tests.
+ logger.info("Finished all backup tests");
+ }
+
+ private void testMetadata(
+ OperationFuture op1,
+ OperationFuture op2,
+ String backupId1,
+ String backupId2,
+ Database db1,
+ Database db2)
+ throws InterruptedException, ExecutionException {
+
+ logger.info("Getting operation metadata 1");
+ CreateBackupMetadata metadata1 = op1.getMetadata().get();
+ logger.info("Getting operation metadata 2");
+ CreateBackupMetadata metadata2 = op2.getMetadata().get();
+ String expectedOperationName1 =
+ String.format(EXPECTED_OP_NAME_FORMAT, testHelper.getInstanceId().getName(), backupId1);
+ String expectedOperationName2 =
+ String.format(EXPECTED_OP_NAME_FORMAT, testHelper.getInstanceId().getName(), backupId2);
+ assertThat(op1.getName()).startsWith(expectedOperationName1);
+ assertThat(op2.getName()).startsWith(expectedOperationName2);
+ assertThat(metadata1.getDatabase()).isEqualTo(db1.getId().getName());
+ assertThat(metadata2.getDatabase()).isEqualTo(db2.getId().getName());
+ assertThat(metadata1.getName())
+ .isEqualTo(BackupId.of(testHelper.getInstanceId(), backupId1).getName());
+ assertThat(metadata2.getName())
+ .isEqualTo(BackupId.of(testHelper.getInstanceId(), backupId2).getName());
+ }
+
+ private void testCreateInvalidExpirationDate(Database db) throws InterruptedException {
+ // This is not allowed, the expiration date must be at least 6 hours in the future.
+ Timestamp expireTime = yesterday();
+ String backupId = getUniqueBackupId();
+ logger.info(String.format("Creating backup %s with invalid expiration date", backupId));
+ OperationFuture op =
+ dbAdminClient.createBackup(
+ testHelper.getInstanceId().getInstance(),
+ backupId,
+ db.getId().getDatabase(),
+ expireTime);
+ backups.add(backupId);
+ try {
+ op.get();
+ fail("missing expected exception");
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ assertThat(cause).isInstanceOf(SpannerException.class);
+ SpannerException se = (SpannerException) cause;
+ assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT);
+ }
+ }
+
+ private void testCancelBackupOperation(Database db)
+ throws InterruptedException, ExecutionException {
+ Timestamp expireTime = after7Days();
+ String backupId = getUniqueBackupId();
+ logger.info(String.format("Starting to create backup %s", backupId));
+ OperationFuture op =
+ dbAdminClient.createBackup(
+ testHelper.getInstanceId().getInstance(),
+ backupId,
+ db.getId().getDatabase(),
+ expireTime);
+ backups.add(backupId);
+ // Cancel the backup operation.
+ logger.info(String.format("Cancelling the creation of backup %s", backupId));
+ dbAdminClient.cancelOperation(op.getName());
+ logger.info("Fetching backup operations");
+ boolean operationFound = false;
+ for (Operation operation :
+ dbAdminClient
+ .listBackupOperations(
+ testHelper.getInstanceId().getInstance(),
+ Options.filter(String.format("name:%s", op.getName())))
+ .iterateAll()) {
+ assertThat(operation.getError().getCode()).isEqualTo(Status.Code.CANCELLED.value());
+ operationFound = true;
+ }
+ assertThat(operationFound).isTrue();
+ logger.info("Finished cancel test");
+ }
+
+ private void testGetBackup(Database db, String backupId, Timestamp expireTime) {
+ // Get the most recent version of the backup.
+ logger.info(String.format("Getting backup %s", backupId));
+ Backup backup = instance.getBackup(backupId);
+ assertThat(backup.getState()).isEqualTo(Backup.State.READY);
+ assertThat(backup.getSize()).isGreaterThan(0L);
+ assertThat(backup.getExpireTime()).isEqualTo(expireTime);
+ assertThat(backup.getDatabase()).isEqualTo(db.getId());
+ }
+
+ private void testUpdateBackup(Backup backup) throws InterruptedException, ExecutionException {
+ // Update the expire time.
+ Timestamp tomorrow = tomorrow();
+ backup = backup.toBuilder().setExpireTime(tomorrow).build();
+ logger.info(
+ String.format("Updating expire time of backup %s to 1 week", backup.getId().getBackup()));
+ backup.updateExpireTime();
+ // Re-get the backup and ensure the expire time was updated.
+ logger.info(String.format("Reloading backup %s", backup.getId().getBackup()));
+ backup = backup.reload();
+ assertThat(backup.getExpireTime()).isEqualTo(tomorrow);
+
+ // Try to set the expire time to 5 minutes in the future.
+ Timestamp in5Minutes = after5Minutes();
+ backup = backup.toBuilder().setExpireTime(in5Minutes).build();
+ try {
+ logger.info(
+ String.format(
+ "Updating expire time of backup %s to 5 minutes", backup.getId().getBackup()));
+ backup.updateExpireTime();
+ fail("Missing expected exception");
+ } catch (SpannerException e) {
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT);
+ }
+ // Re-get the backup and ensure the expire time is still in one week.
+ backup = backup.reload();
+ assertThat(backup.getExpireTime()).isEqualTo(tomorrow);
+ }
+
+ private void testPagination(int expectedMinimumTotalBackups) {
+ logger.info("Listing backups using pagination");
+ int numBackups = 0;
+ logger.info("Fetching first page");
+ Page page =
+ dbAdminClient.listBackups(testHelper.getInstanceId().getInstance(), Options.pageSize(1));
+ assertThat(page.getValues()).hasSize(1);
+ numBackups++;
+ assertThat(page.hasNextPage()).isTrue();
+ while (page.hasNextPage()) {
+ logger.info(String.format("Fetching page %d", numBackups + 1));
+ page =
+ dbAdminClient.listBackups(
+ testHelper.getInstanceId().getInstance(),
+ Options.pageToken(page.getNextPageToken()),
+ Options.pageSize(1));
+ assertThat(page.getValues()).hasSize(1);
+ numBackups++;
+ }
+ assertThat(numBackups).isAtLeast(expectedMinimumTotalBackups);
+ }
+
+ private void testDelete(String backupId) throws InterruptedException, ExecutionException {
+ waitForDbOperations(backupId);
+ // Get the backup.
+ logger.info(String.format("Fetching backup %s", backupId));
+ Backup backup = instance.getBackup(backupId);
+ // Delete it.
+ logger.info(String.format("Deleting backup %s", backupId));
+ backup.delete();
+ // Try to get it again. This should cause a NOT_FOUND error.
+ try {
+ logger.info(String.format("Fetching non-existent backup %s", backupId));
+ instance.getBackup(backupId);
+ fail("Missing expected exception");
+ } catch (SpannerException e) {
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND);
+ }
+ // Try to delete the non-existent backup. This should be a no-op.
+ logger.info(String.format("Deleting non-existent backup %s", backupId));
+ backup.delete();
+ logger.info("Finished delete tests");
+ }
+
+ private void testRestore(Backup backup, OperationFuture backupOp)
+ throws InterruptedException, ExecutionException {
+ // Restore the backup to a new database.
+ String restoredDb = testHelper.getUniqueDatabaseId();
+ logger.info(
+ String.format(
+ "Restoring backup %s to database %s", backup.getId().getBackup(), restoredDb));
+ OperationFuture restoreOp =
+ backup.restore(DatabaseId.of(testHelper.getInstanceId(), restoredDb));
+ databases.add(restoredDb);
+ final String restoreOperationName = restoreOp.getName();
+ logger.info(String.format("Restore operation %s running", restoreOperationName));
+ RestoreDatabaseMetadata metadata = restoreOp.getMetadata().get();
+ assertThat(metadata.getBackupInfo().getBackup()).isEqualTo(backup.getId().getName());
+ assertThat(metadata.getSourceType()).isEqualTo(RestoreSourceType.BACKUP);
+ assertThat(metadata.getName())
+ .isEqualTo(DatabaseId.of(testHelper.getInstanceId(), restoredDb).getName());
+
+ // Ensure the operations show up in the right collections.
+ // TODO: Re-enable when it is clear why this fails on the CI environment.
+ // verifyRestoreOperations(backupOp.getName(), restoreOperationName);
+
+ // Wait until the restore operation has finished successfully.
+ Database database = restoreOp.get();
+ assertThat(database.getId().getDatabase()).isEqualTo(restoredDb);
+ // Restoring the backup to an existing database should fail.
+ try {
+ logger.info(
+ String.format(
+ "Restoring backup %s to existing database %s",
+ backup.getId().getBackup(), restoredDb));
+ backup.restore(DatabaseId.of(testHelper.getInstanceId(), restoredDb)).get();
+ fail("Missing expected exception");
+ } catch (ExecutionException ee) {
+ assertThat(ee.getCause()).isInstanceOf(SpannerException.class);
+ SpannerException e = (SpannerException) ee.getCause();
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS);
+ }
+ }
+
+ // TODO: Remove when this verification can be re-enabled.
+ @SuppressWarnings("unused")
+ private void verifyRestoreOperations(
+ final String backupOperationName, final String restoreOperationName) {
+ assertThat(
+ Iterables.any(
+ instance.listBackupOperations().iterateAll(),
+ new Predicate() {
+ @Override
+ public boolean apply(Operation input) {
+ return input.getName().equals(backupOperationName);
+ }
+ }))
+ .isTrue();
+ assertThat(
+ Iterables.any(
+ instance.listBackupOperations().iterateAll(),
+ new Predicate() {
+ @Override
+ public boolean apply(Operation input) {
+ return input.getName().equals(restoreOperationName);
+ }
+ }))
+ .isFalse();
+ assertThat(
+ Iterables.any(
+ instance.listDatabaseOperations().iterateAll(),
+ new Predicate() {
+ @Override
+ public boolean apply(Operation input) {
+ return input.getName().equals(backupOperationName);
+ }
+ }))
+ .isFalse();
+ assertThat(
+ Iterables.any(
+ instance.listDatabaseOperations().iterateAll(),
+ new Predicate() {
+ @Override
+ public boolean apply(Operation input) {
+ return input.getName().equals(restoreOperationName);
+ }
+ }))
+ .isTrue();
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java
index c5090d10e4..b7fc530799 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java
@@ -22,8 +22,8 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.SpannerBatchUpdateException;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Statement;
@@ -45,7 +45,7 @@
import org.junit.runners.JUnit4;
/** Integration tests for DML. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public final class ITBatchDmlTest {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java
index 33f9b68680..9c3f11d3ee 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java
@@ -25,10 +25,10 @@
import com.google.cloud.spanner.BatchTransactionId;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.KeySet;
import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.Partition;
import com.google.cloud.spanner.PartitionOptions;
import com.google.cloud.spanner.ResultSet;
@@ -56,7 +56,7 @@
* Integration test reading large amounts of data using the Batch APIs. The size of data ensures
* that multiple paritions are returned by the server.
*/
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITBatchReadTest {
private static int numRows;
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 043430a2cc..c3be062bfa 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
@@ -20,9 +20,9 @@
import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.Database;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestWithClosedSessionsEnv;
import com.google.cloud.spanner.IntegrationTestWithClosedSessionsEnv.DatabaseClientWithClosedSessionImpl;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SessionNotFoundException;
@@ -44,7 +44,7 @@
import org.junit.runners.JUnit4;
/** Test the automatic re-creation of sessions that have been invalidated by the server. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITClosedSessionTest {
// Run each test case twice to ensure that a retried session does not affect subsequent
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java
index 0dec8ae58b..d9da0f3ef0 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java
@@ -24,10 +24,10 @@
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.Struct;
import com.google.cloud.spanner.TimestampBound;
@@ -48,7 +48,7 @@
import org.threeten.bp.Instant;
/** Integration test for commit timestamp of Cloud Spanner. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITCommitTimestampTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
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 aff4dace54..5610cf336a 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
@@ -23,12 +23,12 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
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.ParallelIntegrationTest;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
@@ -46,7 +46,7 @@
import org.junit.runners.JUnit4;
/** Integration tests for DML. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public final class ITDMLTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java
index bbeed5aef0..574d9c9b15 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java
@@ -24,9 +24,9 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.Options;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.testing.RemoteSpannerHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -46,7 +46,7 @@
import org.junit.runners.JUnit4;
/** Integration tests for {@link com.google.cloud.spanner.DatabaseAdminClient}. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITDatabaseAdminTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java
index d24c56661f..3a7125312b 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java
@@ -29,8 +29,8 @@
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.InstanceId;
import com.google.cloud.spanner.InstanceNotFoundException;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Statement;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
@@ -44,7 +44,7 @@
import org.junit.runners.JUnit4;
/** Integration tests for database admin functionality: DDL etc. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITDatabaseTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITLargeReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITLargeReadTest.java
index 8fa2e05ce8..3fa99c225f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITLargeReadTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITLargeReadTest.java
@@ -21,11 +21,11 @@
import com.google.cloud.ByteArray;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
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.Statement;
import com.google.common.hash.HashFunction;
@@ -46,7 +46,7 @@
* Integration test reading large amounts of data. The size of data ensures that multiple chunks are
* returned by the server.
*/
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITLargeReadTest {
private static int numRows;
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java
index 89d789e6f0..9bc221c553 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java
@@ -22,8 +22,8 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
@@ -40,7 +40,7 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITQueryOptionsTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java
index 2cc92867b6..79e9a5f4ea 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java
@@ -27,9 +27,9 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Statement;
@@ -54,7 +54,7 @@
import org.junit.runners.JUnit4;
/** Integration tests for query execution. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITQueryTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java
index 352219208d..e6e473779d 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java
@@ -21,10 +21,10 @@
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.ReadContext;
import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.ResultSet;
@@ -55,7 +55,7 @@
* distinguished. Hence, these integration tests only minimally verify that read-only transactions
* work at all, and unit tests are relied on for validating that modes are encoded correctly.
*/
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITReadOnlyTxnTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java
index b673d05e1e..b06d3ae152 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java
@@ -25,13 +25,13 @@
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
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;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Struct;
@@ -63,7 +63,7 @@
* See also {@link ITWriteTest}, which provides coverage of writing and reading back all Cloud
* Spanner types.
*/
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITReadTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java
index 08cdabd1f3..ed7442a237 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java
@@ -24,8 +24,8 @@
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.InstanceAdminClient;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerOptions;
@@ -45,7 +45,7 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITSpannerOptionsTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
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 abda78dd3e..12d7ec2747 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
@@ -22,10 +22,10 @@
import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
-import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ParallelIntegrationTest;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Struct;
import com.google.cloud.spanner.TransactionContext;
@@ -41,7 +41,7 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITTransactionManagerTest {
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 a442e4d2ec..4e95f8efe8 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
@@ -28,11 +28,11 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
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.PartitionOptions;
import com.google.cloud.spanner.ReadContext;
import com.google.cloud.spanner.ResultSet;
@@ -58,7 +58,7 @@
import org.junit.runners.JUnit4;
/** Integration tests for read-write transactions. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITTransactionTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java
index fed755c588..0a4dd79d5b 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java
@@ -26,11 +26,11 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.IntegrationTest;
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.ResultSet;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Struct;
@@ -57,7 +57,7 @@
import org.junit.runners.JUnit4;
/** Integration test for writing data to Cloud Spanner. */
-@Category(IntegrationTest.class)
+@Category(ParallelIntegrationTest.class)
@RunWith(JUnit4.class)
public class ITWriteTest {
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();