From d72c2f79f8cf0b83da00060587a079ce859c87a2 Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Mon, 19 Jul 2021 11:26:32 +1000 Subject: [PATCH] feat: exposes default leader in database, and leader options / replicas in instance config (#1283) * feat: expose leader options in instance config Exposes the list of leader options for an instance from an instance config. * feat: expose default leader in database * test: add it test for instance leader options * test: add it test for database default leader * feat: create replica info / type model * feat: expose replicas in instance config * test: add it test for replicas in instance config * test: skip leader tests for emulator * test: skip emulator for instance admin test Skips leader options / replicas checking related tests for the emulator. * docs: add link for leader config accepted values * docs: fix the link for replica infos. * test: skip / removes leader tests We decided not to have setting the default leader tests, because it requires a multi-regional instance. The one we currently have for running the tests is regional, so the cost of setup would not justify the testing. We also skip the get leader options test, since the feature is not enabled in production yet. --- .../com/google/cloud/spanner/Database.java | 1 + .../google/cloud/spanner/DatabaseInfo.java | 41 +++- .../google/cloud/spanner/InstanceConfig.java | 22 +- .../cloud/spanner/InstanceConfigInfo.java | 50 ++++- .../com/google/cloud/spanner/ReplicaInfo.java | 194 ++++++++++++++++++ .../google/cloud/spanner/DatabaseTest.java | 42 ++-- .../cloud/spanner/InstanceAdminGaxTest.java | 9 +- .../cloud/spanner/InstanceConfigTest.java | 104 ++++++++++ .../google/cloud/spanner/ReplicaInfoTest.java | 57 +++++ .../google/cloud/spanner/ReplicaTypeTest.java | 66 ++++++ .../cloud/spanner/it/ITInstanceAdminTest.java | 12 ++ 11 files changed, 561 insertions(+), 37 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReplicaInfo.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaInfoTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaTypeTest.java 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 05ba3f2edf..30d5017272 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 @@ -187,6 +187,7 @@ static Database fromProto( .setVersionRetentionPeriod(proto.getVersionRetentionPeriod()) .setEarliestVersionTime(Timestamp.fromProto(proto.getEarliestVersionTime())) .setEncryptionConfig(CustomerManagedEncryption.fromProtoOrNull(proto.getEncryptionConfig())) + .setDefaultLeader(proto.getDefaultLeader()) .setProto(proto) .build(); } 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 101fdd4e64..b58769bcb5 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 @@ -24,6 +24,7 @@ /** Represents a Cloud Spanner database. */ public class DatabaseInfo { + public abstract static class Builder { abstract Builder setState(State state); @@ -44,6 +45,15 @@ public abstract static class Builder { */ public abstract Builder setEncryptionConfig(CustomerManagedEncryption encryptionConfig); + /** + * The read-write region which will be used for the database's leader replicas. This can be one + * of the values as specified in + * https://cloud.google.com/spanner/docs/instances#available-configurations-multi-region. + */ + public Builder setDefaultLeader(String defaultLeader) { + throw new UnsupportedOperationException("Unimplemented"); + } + abstract Builder setProto(com.google.spanner.admin.database.v1.Database proto); /** Builds the database from this builder. */ @@ -58,6 +68,7 @@ abstract static class BuilderImpl extends Builder { private String versionRetentionPeriod; private Timestamp earliestVersionTime; private CustomerManagedEncryption encryptionConfig; + private String defaultLeader; private com.google.spanner.admin.database.v1.Database proto; BuilderImpl(DatabaseId id) { @@ -72,6 +83,7 @@ abstract static class BuilderImpl extends Builder { this.versionRetentionPeriod = other.versionRetentionPeriod; this.earliestVersionTime = other.earliestVersionTime; this.encryptionConfig = other.encryptionConfig; + this.defaultLeader = other.defaultLeader; this.proto = other.proto; } @@ -111,6 +123,12 @@ public Builder setEncryptionConfig(@Nullable CustomerManagedEncryption encryptio return this; } + @Override + public Builder setDefaultLeader(String defaultLeader) { + this.defaultLeader = defaultLeader; + return this; + } + @Override Builder setProto(@Nullable com.google.spanner.admin.database.v1.Database proto) { this.proto = proto; @@ -137,6 +155,7 @@ public enum State { private final String versionRetentionPeriod; private final Timestamp earliestVersionTime; private final CustomerManagedEncryption encryptionConfig; + private final String defaultLeader; private final com.google.spanner.admin.database.v1.Database proto; public DatabaseInfo(DatabaseId id, State state) { @@ -147,6 +166,7 @@ public DatabaseInfo(DatabaseId id, State state) { this.versionRetentionPeriod = null; this.earliestVersionTime = null; this.encryptionConfig = null; + this.defaultLeader = null; this.proto = null; } @@ -158,6 +178,7 @@ public DatabaseInfo(DatabaseId id, State state) { this.versionRetentionPeriod = builder.versionRetentionPeriod; this.earliestVersionTime = builder.earliestVersionTime; this.encryptionConfig = builder.encryptionConfig; + this.defaultLeader = builder.defaultLeader; this.proto = builder.proto; } @@ -209,6 +230,15 @@ public Timestamp getEarliestVersionTime() { return encryptionConfig; } + /** + * The read-write region which contains the database's leader replicas. If this value was not + * explicitly set during a create database or update database ddl operations, it will be {@code + * NULL}. + */ + public @Nullable String getDefaultLeader() { + return defaultLeader; + } + /** 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; @@ -229,7 +259,8 @@ public boolean equals(Object o) { && Objects.equals(restoreInfo, that.restoreInfo) && Objects.equals(versionRetentionPeriod, that.versionRetentionPeriod) && Objects.equals(earliestVersionTime, that.earliestVersionTime) - && Objects.equals(encryptionConfig, that.encryptionConfig); + && Objects.equals(encryptionConfig, that.encryptionConfig) + && Objects.equals(defaultLeader, that.defaultLeader); } @Override @@ -241,19 +272,21 @@ public int hashCode() { restoreInfo, versionRetentionPeriod, earliestVersionTime, - encryptionConfig); + encryptionConfig, + defaultLeader); } @Override public String toString() { return String.format( - "Database[%s, %s, %s, %s, %s, %s, %s]", + "Database[%s, %s, %s, %s, %s, %s, %s, %s]", id.getName(), state, createTime, restoreInfo, versionRetentionPeriod, earliestVersionTime, - encryptionConfig); + encryptionConfig, + defaultLeader); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java index 1076fae9c3..6afbc028e1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java @@ -16,6 +16,10 @@ package com.google.cloud.spanner; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + /** * Represents a Cloud Spanner instance config.{@code InstanceConfig} adds a layer of service related * functionality over {@code InstanceConfigInfo}. @@ -25,7 +29,16 @@ public class InstanceConfig extends InstanceConfigInfo { private final InstanceAdminClient client; public InstanceConfig(InstanceConfigId id, String displayName, InstanceAdminClient client) { - super(id, displayName); + this(id, displayName, Collections.emptyList(), Collections.emptyList(), client); + } + + public InstanceConfig( + InstanceConfigId id, + String displayName, + List replicas, + List leaderOptions, + InstanceAdminClient client) { + super(id, displayName, replicas, leaderOptions); this.client = client; } @@ -36,6 +49,11 @@ public InstanceConfig reload() { static InstanceConfig fromProto( com.google.spanner.admin.instance.v1.InstanceConfig proto, InstanceAdminClient client) { - return new InstanceConfig(InstanceConfigId.of(proto.getName()), proto.getDisplayName(), client); + return new InstanceConfig( + InstanceConfigId.of(proto.getName()), + proto.getDisplayName(), + proto.getReplicasList().stream().map(ReplicaInfo::fromProto).collect(Collectors.toList()), + proto.getLeaderOptionsList(), + client); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java index 7b391e4c5d..43d4fbf953 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** Represents a Cloud Spanner instance config resource. */ @@ -23,15 +25,25 @@ public class InstanceConfigInfo { private final InstanceConfigId id; private final String displayName; + private final List replicas; + private final List leaderOptions; public InstanceConfigInfo(InstanceConfigId id, String displayName) { + this(id, displayName, Collections.emptyList(), Collections.emptyList()); + } + + public InstanceConfigInfo( + InstanceConfigId id, + String displayName, + List replicas, + List leaderOptions) { this.id = id; this.displayName = displayName; + this.replicas = replicas; + this.leaderOptions = leaderOptions; } - /* - * Returns the id of this instance config. - */ + /** Returns the id of this instance config. */ public InstanceConfigId getId() { return id; } @@ -41,9 +53,20 @@ public String getDisplayName() { return displayName; } - @Override - public int hashCode() { - return Objects.hash(id, displayName); + /** + * The geographic placement of nodes in this instance configuration and their replication + * properties. + */ + public List getReplicas() { + return replicas; + } + + /** + * Allowed values of the default leader schema option for databases in instances that use this + * instance configuration. + */ + public List getLeaderOptions() { + return leaderOptions; } @Override @@ -51,15 +74,24 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof InstanceConfigInfo)) { return false; } InstanceConfigInfo that = (InstanceConfigInfo) o; - return that.id.equals(id) && that.displayName.equals(displayName); + return Objects.equals(id, that.id) + && Objects.equals(displayName, that.displayName) + && Objects.equals(replicas, that.replicas) + && Objects.equals(leaderOptions, that.leaderOptions); + } + + @Override + public int hashCode() { + return Objects.hash(id, displayName, replicas, leaderOptions); } @Override public String toString() { - return String.format("Instance Config[%s, %s]", id, displayName); + return String.format( + "Instance Config[%s, %s, %s, %s]", id, displayName, replicas, leaderOptions); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReplicaInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReplicaInfo.java new file mode 100644 index 0000000000..4d3887c0ad --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReplicaInfo.java @@ -0,0 +1,194 @@ +/* + * Copyright 2021 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.common.annotations.VisibleForTesting; +import java.util.Objects; + +/** Represents a Cloud Spanner replica information. */ +public class ReplicaInfo { + + abstract static class Builder { + abstract Builder setLocation(String location); + + abstract Builder setType(ReplicaType type); + + abstract Builder setDefaultLeaderLocation(boolean defaultLeaderLocation); + + abstract Builder setProto(com.google.spanner.admin.instance.v1.ReplicaInfo proto); + + public abstract ReplicaInfo build(); + } + + public static class BuilderImpl extends Builder { + + private String location; + private ReplicaType type; + private boolean defaultLeaderLocation; + private com.google.spanner.admin.instance.v1.ReplicaInfo proto; + + /** + * The location of the serving resources. This can be one of the values as specified in + * https://cloud.google.com/spanner/docs/instances#available-configurations-regional. + */ + @Override + Builder setLocation(String location) { + this.location = location; + return this; + } + + /** The type of the replica, as per {@link ReplicaType}. */ + @Override + Builder setType(ReplicaType type) { + this.type = type; + return this; + } + + /** + * If true, this location is designated as the default leader location where leader replicas are + * placed. + */ + @Override + Builder setDefaultLeaderLocation(boolean defaultLeaderLocation) { + this.defaultLeaderLocation = defaultLeaderLocation; + return this; + } + + @Override + Builder setProto(com.google.spanner.admin.instance.v1.ReplicaInfo proto) { + this.proto = proto; + return this; + } + + @Override + public ReplicaInfo build() { + return new ReplicaInfo(this); + } + } + + public static ReplicaInfo fromProto(com.google.spanner.admin.instance.v1.ReplicaInfo proto) { + return newBuilder() + .setLocation(proto.getLocation()) + .setType(ReplicaType.fromProto(proto.getType())) + .setDefaultLeaderLocation(proto.getDefaultLeaderLocation()) + .setProto(proto) + .build(); + } + + static Builder newBuilder() { + return new BuilderImpl(); + } + + private final String location; + private final ReplicaType type; + private final boolean defaultLeaderLocation; + private final com.google.spanner.admin.instance.v1.ReplicaInfo proto; + + @VisibleForTesting + ReplicaInfo( + String location, + ReplicaType type, + boolean defaultLeaderLocation, + com.google.spanner.admin.instance.v1.ReplicaInfo proto) { + this.location = location; + this.type = type; + this.defaultLeaderLocation = defaultLeaderLocation; + this.proto = proto; + } + + ReplicaInfo(BuilderImpl builder) { + this.location = builder.location; + this.type = builder.type; + this.defaultLeaderLocation = builder.defaultLeaderLocation; + this.proto = builder.proto; + } + + public String getLocation() { + return location; + } + + public ReplicaType getType() { + return type; + } + + public boolean isDefaultLeaderLocation() { + return defaultLeaderLocation; + } + + public com.google.spanner.admin.instance.v1.ReplicaInfo getProto() { + return proto; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ReplicaInfo)) { + return false; + } + ReplicaInfo that = (ReplicaInfo) o; + return defaultLeaderLocation == that.defaultLeaderLocation + && Objects.equals(location, that.location) + && type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(location, type, defaultLeaderLocation); + } + + @Override + public String toString() { + return "ReplicaInfo{" + + "location='" + + location + + '\'' + + ", type=" + + type + + ", defaultLeaderLocation=" + + defaultLeaderLocation + + '}'; + } + + /** + * Indicates the type of the replica. See the replica types documentation at + * https://cloud.google.com/spanner/docs/replication#replica_types for more details. + */ + public enum ReplicaType { + TYPE_UNSPECIFIED, + READ_WRITE, + READ_ONLY, + WITNESS; + + public static ReplicaType fromProto( + com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType proto) { + switch (proto) { + case TYPE_UNSPECIFIED: + return ReplicaType.TYPE_UNSPECIFIED; + case READ_WRITE: + return ReplicaType.READ_WRITE; + case READ_ONLY: + return ReplicaType.READ_ONLY; + case WITNESS: + return ReplicaType.WITNESS; + default: + throw new IllegalArgumentException("Unrecognized replica type " + proto); + } + } + } +} 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 e29b457cc9..d1e8d2e31f 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 @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.DatabaseInfo.State.CREATING; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.verify; @@ -61,6 +62,7 @@ public class DatabaseTest { .setEncryptionStatus(Status.newBuilder().setCode(Code.OK.getNumber())) .setKmsKeyVersion(KMS_KEY_VERSION) .build()); + private static final String DEFAULT_LEADER = "default-leader"; @Mock DatabaseAdminClient dbClient; @@ -100,27 +102,13 @@ public void listDatabaseOperations() { @Test public void fromProto() { Database db = createDatabase(); - assertThat(db.getId().getName()).isEqualTo(NAME); - assertThat(db.getState()).isEqualTo(DatabaseInfo.State.CREATING); - assertThat(db.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD); - assertThat(db.getEarliestVersionTime()).isEqualTo(EARLIEST_VERSION_TIME); - assertThat(db.getEncryptionConfig()) - .isEqualTo(EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME)); - } - - @Test - public void testFromProtoWithEncryptionConfig() { - com.google.spanner.admin.database.v1.Database proto = - com.google.spanner.admin.database.v1.Database.newBuilder() - .setName(NAME) - .setEncryptionConfig( - com.google.spanner.admin.database.v1.EncryptionConfig.newBuilder() - .setKmsKeyName("some-key") - .build()) - .build(); - Database db = Database.fromProto(proto, dbClient); - assertThat(db.getEncryptionConfig()).isNotNull(); - assertThat(db.getEncryptionConfig().getKmsKeyName()).isEqualTo("some-key"); + assertEquals(NAME, db.getId().getName()); + assertEquals(CREATING, db.getState()); + assertEquals(VERSION_RETENTION_PERIOD, db.getVersionRetentionPeriod()); + assertEquals(EARLIEST_VERSION_TIME, db.getEarliestVersionTime()); + assertEquals( + EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME), db.getEncryptionConfig()); + assertEquals(DEFAULT_LEADER, db.getDefaultLeader()); } @Test @@ -138,6 +126,17 @@ public void testBuildWithEncryptionConfig() { "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key"); } + @Test + public void testBuildWithDefaultLeader() { + Database db = + dbClient + .newDatabaseBuilder(DatabaseId.of("my-project", "my-instance", "my-database")) + .setDefaultLeader(DEFAULT_LEADER) + .build(); + + assertEquals(DEFAULT_LEADER, db.getDefaultLeader()); + } + @Test public void getIAMPolicy() { Database database = @@ -186,6 +185,7 @@ private Database createDatabase() { .setVersionRetentionPeriod(VERSION_RETENTION_PERIOD) .setEncryptionConfig(ENCRYPTION_CONFIG) .addAllEncryptionInfo(ENCRYPTION_INFOS) + .setDefaultLeader(DEFAULT_LEADER) .build(); return Database.fromProto(proto, dbClient); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java index 6edb9fffde..c5a317ce5d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java @@ -45,6 +45,7 @@ import io.grpc.inprocess.InProcessServerBuilder; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -353,8 +354,13 @@ public void getInstanceConfigTest() { for (int i = 0; i < 2; i++) { InstanceConfigName name2 = InstanceConfigName.of(PROJECT, "INSTANCE_CONFIG"); String displayName = "displayName1615086568"; + List leaderOptions = Arrays.asList("leader option 1", "leader option 2"); InstanceConfig expectedResponse = - InstanceConfig.newBuilder().setName(name2.toString()).setDisplayName(displayName).build(); + InstanceConfig.newBuilder() + .setName(name2.toString()) + .setDisplayName(displayName) + .addAllLeaderOptions(leaderOptions) + .build(); if (exceptionAtCall == 0) { mockInstanceAdmin.addException(exception); } @@ -368,6 +374,7 @@ public void getInstanceConfigTest() { client.getInstanceConfig(name.toString()); Assert.assertEquals(displayName, actualResponse.getDisplayName()); + Assert.assertEquals(leaderOptions, actualResponse.getLeaderOptions()); List actualRequests = mockInstanceAdmin.getRequests(); Assert.assertEquals(i + 1, actualRequests.size()); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java new file mode 100644 index 0000000000..3b0f56c088 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2021 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 org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; + +public class InstanceConfigTest { + + private InstanceAdminClient client; + + @Before + public void setUp() { + client = mock(InstanceAdminClient.class); + } + + @Test + public void testInstanceConfigFromProto() { + final InstanceConfig instanceConfig = + InstanceConfig.fromProto( + com.google.spanner.admin.instance.v1.InstanceConfig.newBuilder() + .setDisplayName("Display Name") + .setName("projects/my-project/instanceConfigs/my-instance-config") + .addAllLeaderOptions(Arrays.asList("Leader Option 1", "Leader Option 2")) + .addAllReplicas( + Arrays.asList( + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Replica Location 1") + .setType(ReplicaType.READ_WRITE) + .setDefaultLeaderLocation(true) + .build(), + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Replica Location 2") + .setType(ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(false) + .build(), + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Replica Location 3") + .setType(ReplicaType.WITNESS) + .setDefaultLeaderLocation(false) + .build())) + .build(), + client); + + assertEquals( + new InstanceConfig( + InstanceConfigId.of("my-project", "my-instance-config"), + "Display Name", + Arrays.asList( + ReplicaInfo.newBuilder() + .setLocation("Replica Location 1") + .setType(ReplicaInfo.ReplicaType.READ_WRITE) + .setDefaultLeaderLocation(true) + .build(), + ReplicaInfo.newBuilder() + .setLocation("Replica Location 2") + .setType(ReplicaInfo.ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(false) + .build(), + ReplicaInfo.newBuilder() + .setLocation("Replica Location 3") + .setType(ReplicaInfo.ReplicaType.WITNESS) + .setDefaultLeaderLocation(false) + .build()), + Arrays.asList("Leader Option 1", "Leader Option 2"), + client), + instanceConfig); + } + + @Test + public void testInstanceConfigFromProtoWithoutReplicasAndLeaderOptions() { + final InstanceConfig instanceConfig = + InstanceConfig.fromProto( + com.google.spanner.admin.instance.v1.InstanceConfig.newBuilder() + .setDisplayName("Display Name") + .setName("projects/my-project/instanceConfigs/my-instance-config") + .build(), + client); + + assertEquals( + new InstanceConfig( + InstanceConfigId.of("my-project", "my-instance-config"), "Display Name", client), + instanceConfig); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaInfoTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaInfoTest.java new file mode 100644 index 0000000000..96005791f3 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaInfoTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.cloud.spanner.ReplicaInfo.ReplicaType.READ_WRITE; +import static org.junit.Assert.assertEquals; + +import com.google.cloud.spanner.ReplicaInfo.ReplicaType; +import org.junit.Test; + +public class ReplicaInfoTest { + + @Test + public void testBuildReplicaInfo() { + final String location = "Location"; + final ReplicaType type = READ_WRITE; + final boolean defaultLeaderLocation = true; + final com.google.spanner.admin.instance.v1.ReplicaInfo proto = + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder().build(); + + assertEquals( + new ReplicaInfo(location, type, defaultLeaderLocation, proto), + ReplicaInfo.newBuilder() + .setLocation(location) + .setType(type) + .setDefaultLeaderLocation(defaultLeaderLocation) + .setProto(proto) + .build()); + } + + @Test + public void testFromProto() { + final com.google.spanner.admin.instance.v1.ReplicaInfo proto = + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Location") + .setType(com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.READ_WRITE) + .setDefaultLeaderLocation(true) + .build(); + + assertEquals( + new ReplicaInfo("Location", READ_WRITE, true, proto), ReplicaInfo.fromProto(proto)); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaTypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaTypeTest.java new file mode 100644 index 0000000000..065a94ba22 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReplicaTypeTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 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 org.junit.Assert.assertEquals; + +import com.google.cloud.spanner.ReplicaInfo.ReplicaType; +import org.junit.Test; + +public class ReplicaTypeTest { + + @Test + public void testTypeUnspecifiedReplicaType() { + final ReplicaType replicaType = + ReplicaType.fromProto( + com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.TYPE_UNSPECIFIED); + + assertEquals(ReplicaType.TYPE_UNSPECIFIED, replicaType); + } + + @Test + public void testReadWriteReplicaType() { + final ReplicaType replicaType = + ReplicaType.fromProto( + com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.READ_WRITE); + + assertEquals(ReplicaType.READ_WRITE, replicaType); + } + + @Test + public void testReadOnlyReplicaType() { + final ReplicaType replicaType = + ReplicaType.fromProto( + com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.READ_ONLY); + + assertEquals(ReplicaType.READ_ONLY, replicaType); + } + + @Test + public void testWitnessReplicaType() { + final ReplicaType replicaType = + ReplicaType.fromProto(com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.WITNESS); + + assertEquals(ReplicaType.WITNESS, replicaType); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnrecognizedReplicaType() { + ReplicaType.fromProto( + com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.UNRECOGNIZED); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java index 340f0a5958..fcda2603ca 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java @@ -35,6 +35,7 @@ import java.util.Random; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @@ -64,6 +65,17 @@ public void instanceConfigOperations() { assertThat(config.getId()).isEqualTo(configs.get(0).getId()); } + @Ignore("Feature is not yet enabled in production") + @Test + public void instanceConfigLeaderOptions() { + assumeFalse("The emulator does not support leader options", isUsingEmulator()); + List configs = new ArrayList<>(); + Iterators.addAll(configs, instanceClient.listInstanceConfigs().iterateAll().iterator()); + + configs.forEach(config -> assertThat(config.getReplicas()).isNotEmpty()); + configs.forEach(config -> assertThat(config.getLeaderOptions()).isNotEmpty()); + } + @Test public void listInstances() { Instance instance =