Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add backup support #100

Merged
merged 28 commits into from Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
753dfce
feat: add backup support
olavloite Mar 13, 2020
c65f25a
fix: add interface changes to ignore
olavloite Mar 13, 2020
7e918ee
tests: fix failing test cases
olavloite Mar 13, 2020
ae90f2b
fix: auto throttle administrative requests
olavloite Mar 16, 2020
4e0ff10
fix: reduce request rate
olavloite Mar 16, 2020
6c73067
tests: increase timeout to allow ITs to pass
olavloite Mar 16, 2020
ead8c35
tests: parallelize ITs to speed up build
olavloite Mar 16, 2020
37d134b
tests: further tuning of ITs
olavloite Mar 16, 2020
9d165f0
tests: increase timeout to test restore op
olavloite Mar 16, 2020
370e26d
logging: add logging of backup and db ids
olavloite Mar 17, 2020
d6b0ae2
fix: simplify restore test to check IT
olavloite Mar 18, 2020
671b951
test: add checking metadata again
olavloite Mar 18, 2020
0ed523e
test: list operations
olavloite Mar 18, 2020
fe89140
test: only list backup operations
olavloite Mar 18, 2020
5b5da55
test: skip listBackupOperations
olavloite Mar 18, 2020
60d1e1f
fix: skip all list operations
olavloite Mar 18, 2020
76003bb
fix: remove custom test env for backups
olavloite Mar 18, 2020
203137a
fix: use polling future instead of initial future
olavloite Mar 18, 2020
bc8e71c
fix: disable get metadata because of timeouts
olavloite Mar 18, 2020
c10f127
fix: remove all metadata tests because of timeouts
olavloite Mar 18, 2020
970288b
fix: re-introduce custom timeout settings
olavloite Mar 18, 2020
b1e07e3
fix: re-introduce custom timeouts
olavloite Mar 18, 2020
5a4ff73
fix: fix invalid retry settings
olavloite Mar 18, 2020
4577acb
test: remove retry codes
olavloite Mar 18, 2020
898835b
test: set custom retry settings based on gapic config
olavloite Mar 18, 2020
26f8c55
fix: update copyright year
olavloite Mar 18, 2020
87c8511
fix: run code formatter
olavloite Mar 18, 2020
6676e66
fix: include working retry settings in default admin client
olavloite Mar 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
136 changes: 136 additions & 0 deletions google-cloud-spanner/clirr-ignored-differences.xml
Expand Up @@ -11,4 +11,140 @@
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>* asyncDeleteSession(*)</method>
</difference>

<!-- Adding Backup feature -->
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>void cancelOperation(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.api.gax.longrunning.OperationFuture createBackup(java.lang.String, java.lang.String, java.lang.String, com.google.cloud.Timestamp)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>void deleteBackup(java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.cloud.spanner.Backup getBackup(java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.cloud.spanner.Backup getBackup(java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.cloud.Policy getBackupIAMPolicy(java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.longrunning.Operation getOperation(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.api.gax.paging.Page listBackupOperations(java.lang.String, com.google.cloud.spanner.Options$ListOption[])</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.api.gax.paging.Page listBackups(java.lang.String, com.google.cloud.spanner.Options$ListOption[])</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.api.gax.paging.Page listDatabaseOperations(java.lang.String, com.google.cloud.spanner.Options$ListOption[])</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.cloud.spanner.Backup$Builder newBackupBuilder(com.google.cloud.spanner.BackupId)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.cloud.spanner.Backup$Builder newBackupBuilder(com.google.cloud.spanner.BackupId)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.api.gax.longrunning.OperationFuture restoreDatabase(java.lang.String, java.lang.String, java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.cloud.Policy setBackupIAMPolicy(java.lang.String, java.lang.String, com.google.cloud.Policy)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>java.lang.Iterable testBackupIAMPermissions(java.lang.String, java.lang.String, java.lang.Iterable)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
<method>com.google.cloud.spanner.Backup updateBackup(java.lang.String, java.lang.String, com.google.cloud.Timestamp)</method>
</difference>

<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>void cancelOperation(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.api.gax.longrunning.OperationFuture createBackup(java.lang.String, java.lang.String, com.google.spanner.admin.database.v1.Backup)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>void deleteBackup(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.spanner.admin.database.v1.Backup getBackup(java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.cloud.spanner.spi.v1.SpannerRpc$Paginated listBackupOperations(java.lang.String, int, java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.cloud.spanner.spi.v1.SpannerRpc$Paginated listBackups(java.lang.String, int, java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.cloud.spanner.spi.v1.SpannerRpc$Paginated listDatabaseOperations(java.lang.String, int, java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.api.gax.longrunning.OperationFuture restoreDatabase(java.lang.String, java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.spanner.admin.database.v1.Backup updateBackup(com.google.spanner.admin.database.v1.Backup, com.google.protobuf.FieldMask)</method>
</difference>

<difference>
<differenceType>7004</differenceType>
<className>com/google/cloud/spanner/Instance</className>
<!-- Added varargs (ListOption... options) -->
<method>com.google.api.gax.paging.Page listDatabases()</method>
</difference>

</differences>
41 changes: 32 additions & 9 deletions google-cloud-spanner/pom.xml
Expand Up @@ -62,6 +62,38 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<spanner.testenv.config.class>com.google.cloud.spanner.GceTestEnvConfig</spanner.testenv.config.class>
<spanner.testenv.instance>projects/gcloud-devel/instances/spanner-testing</spanner.testenv.instance>
</systemPropertyVariables>
<forkedProcessTimeoutInSeconds>3000</forkedProcessTimeoutInSeconds>
</configuration>
<executions>
<execution>
<id>default</id>
<configuration>
<groups>com.google.cloud.spanner.IntegrationTest</groups>
<excludedGroups>com.google.cloud.spanner.FlakyTest,com.google.cloud.spanner.TracerTest,com.google.cloud.spanner.ParallelIntegrationTest</excludedGroups>
</configuration>
</execution>
<execution>
<id>parallel-integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
<configuration>
<groups>com.google.cloud.spanner.ParallelIntegrationTest</groups>
<excludedGroups>com.google.cloud.spanner.FlakyTest,com.google.cloud.spanner.TracerTest,com.google.cloud.spanner.IntegrationTest</excludedGroups>
<forkCount>8</forkCount>
<reuseForks>true</reuseForks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
Expand All @@ -74,15 +106,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M4</version>
<configuration>
<systemPropertyVariables>
<spanner.testenv.config.class>com.google.cloud.spanner.GceTestEnvConfig</spanner.testenv.config.class>
<spanner.testenv.instance>projects/gcloud-devel/instances/spanner-testing</spanner.testenv.instance>
</systemPropertyVariables>
<groups>com.google.cloud.spanner.IntegrationTest</groups>
<excludedGroups>com.google.cloud.spanner.FlakyTest,com.google.cloud.spanner.TracerTest</excludedGroups>
<forkedProcessTimeoutInSeconds>2400</forkedProcessTimeoutInSeconds>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
@@ -0,0 +1,203 @@
/*
* 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.base.Preconditions.checkArgument;

import com.google.api.client.util.Preconditions;
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.longrunning.Operation;
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;

/**
* Represents a Cloud Spanner database backup. {@code Backup} adds a layer of service related
* functionality over {@code BackupInfo}.
*/
public class Backup extends BackupInfo {
public static class Builder extends BackupInfo.BuilderImpl {
private final DatabaseAdminClient dbClient;

Builder(DatabaseAdminClient dbClient, BackupId backupId) {
super(backupId);
this.dbClient = Preconditions.checkNotNull(dbClient);
}

private Builder(Backup backup) {
super(backup);
this.dbClient = backup.dbClient;
}

@Override
public Backup build() {
return new Backup(this);
}
}

private static final String FILTER_BACKUP_OPERATIONS_TEMPLATE = "name:backups/%s";
private final DatabaseAdminClient dbClient;

Backup(Builder builder) {
super(builder);
this.dbClient = Preconditions.checkNotNull(builder.dbClient);
}

/** Creates a backup on the server based on the source of this {@link Backup} instance. */
public OperationFuture<Backup, CreateBackupMetadata> create() {
Preconditions.checkState(
getExpireTime() != null, "Cannot create a backup without an expire time");
Preconditions.checkState(
getDatabase() != null, "Cannot create a backup without a source database");
return dbClient.createBackup(instance(), backup(), sourceDatabase(), getExpireTime());
}

/**
* Returns <code>true</code> if a backup with the id of this {@link Backup} exists on Cloud
* Spanner.
*/
public boolean exists() {
try {
dbClient.getBackup(instance(), backup());
} catch (SpannerException e) {
if (e.getErrorCode() == ErrorCode.NOT_FOUND) {
return false;
}
throw e;
}
return true;
}

/**
* Returns <code>true</code> if this backup is ready to use. The value returned by this method
* could be out-of-sync with the value returned by {@link #getState()}, as this method will make a
* round-trip to the server and return a value based on the response from the server.
*/
public boolean isReady() {
return reload().getState() == State.READY;
}

/**
* Fetches the backup's current information and returns a new {@link Backup} instance. It does not
* update this instance.
*/
public Backup reload() throws SpannerException {
return dbClient.getBackup(instance(), backup());
}

/** Deletes this backup on Cloud Spanner. */
public void delete() throws SpannerException {
dbClient.deleteBackup(instance(), backup());
}

/**
* Updates the expire time of this backup on Cloud Spanner. If this {@link Backup} does not have
* an expire time, the method will throw an {@link IllegalStateException}.
*/
public void updateExpireTime() {
Preconditions.checkState(getExpireTime() != null, "This backup has no expire time");
dbClient.updateBackup(instance(), backup(), getExpireTime());
}

/**
* Restores this backup to the specified database. The database must not already exist and will be
* created by this call. The database may be created in a different instance than where the backup
* is stored.
*/
public OperationFuture<Database, RestoreDatabaseMetadata> restore(DatabaseId database) {
Preconditions.checkNotNull(database);
return dbClient.restoreDatabase(
instance(), backup(), database.getInstanceId().getInstance(), database.getDatabase());
}

/** Returns all long-running backup operations for this {@link Backup}. */
public Page<Operation> listBackupOperations() {
return dbClient.listBackupOperations(
instance(), Options.filter(String.format(FILTER_BACKUP_OPERATIONS_TEMPLATE, backup())));
}

/** Returns the IAM {@link Policy} for this backup. */
public Policy getIAMPolicy() {
return dbClient.getBackupIAMPolicy(instance(), backup());
}

/**
* Updates the IAM policy for this 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.
*/
public Policy setIAMPolicy(Policy policy) {
return dbClient.setBackupIAMPolicy(instance(), backup(), policy);
}

/**
* Tests for the given permissions on this backup for the caller.
*
* @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.
*/
public Iterable<String> testIAMPermissions(Iterable<String> permissions) {
return dbClient.testBackupIAMPermissions(instance(), backup(), permissions);
}

public Builder toBuilder() {
return new Builder(this);
}

private String instance() {
return getInstanceId().getInstance();
}

private String backup() {
return getId().getBackup();
}

private String sourceDatabase() {
return getDatabase().getDatabase();
}

static Backup fromProto(
com.google.spanner.admin.database.v1.Backup proto, DatabaseAdminClient client) {
checkArgument(!proto.getName().isEmpty(), "Missing expected 'name' field");
checkArgument(!proto.getDatabase().isEmpty(), "Missing expected 'database' field");
return new Backup.Builder(client, BackupId.of(proto.getName()))
.setState(fromProtoState(proto.getState()))
.setSize(proto.getSizeBytes())
.setExpireTime(Timestamp.fromProto(proto.getExpireTime()))
.setDatabase(DatabaseId.of(proto.getDatabase()))
.setProto(proto)
.build();
}

static BackupInfo.State fromProtoState(
com.google.spanner.admin.database.v1.Backup.State protoState) {
switch (protoState) {
case STATE_UNSPECIFIED:
return BackupInfo.State.UNSPECIFIED;
case CREATING:
return BackupInfo.State.CREATING;
case READY:
return BackupInfo.State.READY;
default:
throw new IllegalArgumentException("Unrecognized state " + protoState);
}
}
}