diff --git a/google-cloud-spanner/acceptance/spanner/client_test.rb b/google-cloud-spanner/acceptance/spanner/client_test.rb new file mode 100644 index 000000000000..111388db6175 --- /dev/null +++ b/google-cloud-spanner/acceptance/spanner/client_test.rb @@ -0,0 +1,36 @@ +# 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 +# +# https://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. + +require "spanner_helper" +require "concurrent" + +describe "Spanner Client", :spanner do + let(:spanner) { $spanner } + let(:instance_id) { $spanner_instance_id } + let(:database_id) { $spanner_database_id } + + it "create client connection without resource based routing" do + client = spanner.client instance_id, database_id + client.service.host.must_equal Google::Cloud::Spanner::V1::SpannerClient::SERVICE_ADDRESS + end + + it "create client connection with resource based routing" do + client = spanner.client instance_id, database_id, enable_resource_based_routing: true + client.resource_based_routing_enabled?.must_equal true + instance = spanner.instance instance_id, fields: ["endpoint_uris"] + # Set to default if no endpoint uri present. + host = instance.endpoint_uris.first || Google::Cloud::Spanner::V1::SpannerClient::SERVICE_ADDRESS + client.service.host.must_equal host + end +end diff --git a/google-cloud-spanner/acceptance/spanner/instance_test.rb b/google-cloud-spanner/acceptance/spanner/instance_test.rb index 4a12f9e8f715..838b657ca0db 100644 --- a/google-cloud-spanner/acceptance/spanner/instance_test.rb +++ b/google-cloud-spanner/acceptance/spanner/instance_test.rb @@ -15,6 +15,8 @@ require "spanner_helper" describe "Spanner Instances", :spanner do + let(:instance_id) { $spanner_instance_id } + it "lists and gets instances" do all_instances = spanner.instances.all.to_a all_instances.wont_be :empty? @@ -26,6 +28,28 @@ first_instance.must_be_kind_of Google::Cloud::Spanner::Instance end + describe "get instance" do + it "get all instance fields" do + instance = spanner.instance instance_id + + instance.instance_id.must_equal instance_id + instance.path.wont_be_empty + instance.display_name.wont_be_empty + instance.nodes.must_be :>, 0 + instance.state.must_equal :READY + end + + it "get specified instance fields" do + instance = spanner.instance instance_id, fields: ["name"] + + instance.instance_id.must_equal instance_id + instance.path.wont_be_empty + instance.display_name.must_be_empty + instance.nodes.must_equal 0 + instance.state.must_equal :STATE_UNSPECIFIED + end + end + describe "IAM Policies and Permissions" do let(:service_account) { spanner.service.credentials.client.issuer } diff --git a/google-cloud-spanner/acceptance/spanner_helper.rb b/google-cloud-spanner/acceptance/spanner_helper.rb index 409fa19e926b..72b6c44deac1 100644 --- a/google-cloud-spanner/acceptance/spanner_helper.rb +++ b/google-cloud-spanner/acceptance/spanner_helper.rb @@ -254,9 +254,9 @@ def default_item_rows fixture = Object.new fixture.extend Acceptance::SpannerTest::Fixtures -instance = $spanner.instance "google-cloud-ruby-tests" +instance = $spanner.instance $spanner_instance_id instance ||= begin - inst_job = $spanner.create_instance "google-cloud-ruby-tests", name: "google-cloud-ruby-tests", config: "regional-us-central1", nodes: 1 + inst_job = $spanner.create_instance $spanner_instance_id, name: $spanner_instance_id, config: "regional-us-central1", nodes: 1 inst_job.wait_until_done! fail GRPC::BadStatus.new(inst_job.error.code, inst_job.error.message) if inst_job.error? inst_job.instance diff --git a/google-cloud-spanner/lib/google/cloud/spanner/batch_client.rb b/google-cloud-spanner/lib/google/cloud/spanner/batch_client.rb index c36d538f07e9..c5682f44d4f5 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/batch_client.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/batch_client.rb @@ -17,6 +17,7 @@ require "google/cloud/spanner/project" require "google/cloud/spanner/session" require "google/cloud/spanner/batch_snapshot" +require "google/cloud/spanner/resource_based_routing" module Google module Cloud @@ -63,13 +64,32 @@ module Spanner # new_partition # class BatchClient + # @!parse extend ResourceBasedRouting + include ResourceBasedRouting + + ## @private Service wrapper for batch data client api. + # @return [ClientServiceProxy] + attr_reader :service + ## # @private Creates a new Spanner BatchClient instance. - def initialize project, instance_id, database_id, session_labels: nil + def initialize \ + project, + instance_id, + database_id, + session_labels: nil, + enable_resource_based_routing: false @project = project @instance_id = instance_id @database_id = database_id @session_labels = session_labels + @enable_resource_based_routing = enable_resource_based_routing + + if resource_based_routing_enabled? + @service = resource_based_routing_service + end + + @service ||= @project.service end # The unique identifier for the project. @@ -184,7 +204,7 @@ def batch_snapshot strong: nil, timestamp: nil, read_timestamp: nil, ensure_service! snp_session = session - snp_grpc = @project.service.create_snapshot \ + snp_grpc = @service.create_snapshot \ snp_session.path, strong: strong, timestamp: (timestamp || read_timestamp), staleness: (staleness || exact_staleness) @@ -231,7 +251,7 @@ def batch_snapshot strong: nil, timestamp: nil, read_timestamp: nil, def load_batch_snapshot serialized_snapshot ensure_service! - BatchSnapshot.load serialized_snapshot, service: @project.service + BatchSnapshot.load serialized_snapshot, service: @service end ## @@ -404,12 +424,12 @@ def ensure_service! # New session for each use. def session ensure_service! - grpc = @project.service.create_session \ + grpc = @service.create_session \ Admin::Database::V1::DatabaseAdminClient.database_path( project_id, instance_id, database_id ), labels: @session_labels - Session.from_grpc grpc, @project.service + Session.from_grpc grpc, @service end ## diff --git a/google-cloud-spanner/lib/google/cloud/spanner/client.rb b/google-cloud-spanner/lib/google/cloud/spanner/client.rb index 5cc3550bd73f..aa6e383de8f5 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/client.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/client.rb @@ -23,6 +23,7 @@ require "google/cloud/spanner/range" require "google/cloud/spanner/column_value" require "google/cloud/spanner/convert" +require "google/cloud/spanner/resource_based_routing" module Google module Cloud @@ -49,22 +50,57 @@ module Spanner # end # end # + # @example Enable resource based routing. + # + # require "google/cloud" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client( + # "my-instance", + # "my-database", + # enable_resource_based_routing: true + # ) + # + # db.transaction do |tx| + # results = tx.execute_query "SELECT * FROM users" + # + # results.rows.each do |row| + # puts "User #{row[:id]} is #{row[:name]}" + # end + # end + # class Client + # @!parse extend ResourceBasedRouting + include ResourceBasedRouting + + ## + # @private The Service object. + # @return [Spanner::Service] + attr_reader :service + ## # @private Creates a new Spanner Client instance. def initialize project, instance_id, database_id, session_labels: nil, - pool_opts: {} + pool_opts: {}, enable_resource_based_routing: false @project = project @instance_id = instance_id @database_id = database_id @session_labels = session_labels + @enable_resource_based_routing = enable_resource_based_routing + + if resource_based_routing_enabled? + @service = resource_based_routing_service + end + + @service ||= @project.service @pool = Pool.new self, pool_opts end # The unique identifier for the project. # @return [String] def project_id - @project.service.project + @service.project end # The unique identifier for the instance. @@ -920,8 +956,7 @@ def commit &block end end - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize,Metrics/MethodLength ## # Creates a transaction for reads and writes that execute atomically at @@ -994,7 +1029,7 @@ def transaction deadline: 120 begin Thread.current[:transaction_id] = tx.transaction_id yield tx - commit_resp = @project.service.commit \ + commit_resp = @service.commit \ tx.session.path, tx.mutations, transaction_id: tx.transaction_id return Convert.timestamp_to_time commit_resp.commit_timestamp rescue GRPC::Aborted, Google::Cloud::AbortedError => err @@ -1022,9 +1057,7 @@ def transaction deadline: 120 end end end - - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize,Metrics/MethodLength ## # Creates a snapshot read-only transaction for reads that execute @@ -1093,7 +1126,7 @@ def snapshot strong: nil, timestamp: nil, read_timestamp: nil, @pool.with_session do |session| begin - snp_grpc = @project.service.create_snapshot \ + snp_grpc = @service.create_snapshot \ session.path, strong: strong, timestamp: (timestamp || read_timestamp), staleness: (staleness || exact_staleness) @@ -1286,12 +1319,12 @@ def reset # Creates a new session object every time. def create_new_session ensure_service! - grpc = @project.service.create_session \ + grpc = @service.create_session \ Admin::Database::V1::DatabaseAdminClient.database_path( project_id, instance_id, database_id ), labels: @session_labels - Session.from_grpc grpc, @project.service + Session.from_grpc grpc, @service end ## @@ -1314,13 +1347,13 @@ def batch_create_new_sessions total # def batch_create_sessions session_count ensure_service! - resp = @project.service.batch_create_sessions \ + resp = @service.batch_create_sessions \ Admin::Database::V1::DatabaseAdminClient.database_path( project_id, instance_id, database_id ), session_count, labels: @session_labels - resp.session.map { |grpc| Session.from_grpc grpc, @project.service } + resp.session.map { |grpc| Session.from_grpc grpc, @service } end # @private @@ -1340,7 +1373,7 @@ def inspect # @private Raise an error unless an active connection to the service is # available. def ensure_service! - raise "Must have active connection to service" unless @project.service + raise "Must have active connection to service" unless @service end ## @@ -1385,7 +1418,7 @@ def single_use_transaction opts end def pdml_transaction session - pdml_tx_grpc = @project.service.create_pdml session.path + pdml_tx_grpc = @service.create_pdml session.path Google::Spanner::V1::TransactionSelector.new id: pdml_tx_grpc.id end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/instance.rb b/google-cloud-spanner/lib/google/cloud/spanner/instance.rb index 551abc01537a..92d1be7bc060 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/instance.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/instance.rb @@ -188,6 +188,20 @@ def labels= labels ) end + ## + # The endpoint URIs based on the instance config. + # For example, instances located in a specific cloud region + # (or multi region) such as nam3, would have a nam3 specific + # endpoint URI. These endpoints are intended to optimize the network + # routing between the client and the instance's serving resources. + # If multiple endpoints are present, client may establish connections + # using any of the given URLs. + # + # @return [Array] The list of URIs. + def endpoint_uris + @grpc.endpoint_uris + end + def save job_grpc = service.update_instance @grpc Instance::Job.from_grpc job_grpc, service diff --git a/google-cloud-spanner/lib/google/cloud/spanner/project.rb b/google-cloud-spanner/lib/google/cloud/spanner/project.rb index 229702bac7f4..e1ff181792ec 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/project.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/project.rb @@ -135,7 +135,9 @@ def instances token: nil, max: nil # Retrieves a Cloud Spanner instance by unique identifier. # # @param [String] instance_id The unique identifier for the instance. - # + # @param [Array] fields Specifies the subset of fields that + # should be returned. If fields are not provided then all fields will + # be returned. Optional. # @return [Google::Cloud::Spanner::Instance, nil] The instance, or `nil` # if the instance does not exist. # @@ -151,9 +153,16 @@ def instances token: nil, max: nil # spanner = Google::Cloud::Spanner.new # instance = spanner.instance "non-existing" # nil # - def instance instance_id + # @example Get instance details with specified fields. + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance", fields: ["name"] + # + def instance instance_id, fields: nil ensure_service! - grpc = service.get_instance instance_id + + grpc = service.get_instance instance_id, fields: fields Instance.from_grpc grpc, service rescue Google::Cloud::NotFoundError nil @@ -452,7 +461,11 @@ def create_database instance_id, database_id, statements: [] # * Label values must be between 0 and 63 characters long and must # conform to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. # * No more than 64 labels can be associated with a given resource. - # + # @param [Boolean] enable_resource_based_routing Enable/Disable + # resource-based routing for data operation, by default it is + # disabled. Resource based routing can be enabled using the + # environment `GOOGLE_CLOUD_SPANNER_ENABLE_RESOURCE_BASED_ROUTING` to + # `TRUE` or `true`. # @return [Client] The newly created client. # # @example @@ -470,12 +483,37 @@ def create_database instance_id, database_id, statements: [] # end # end # - def client instance_id, database_id, pool: {}, labels: nil + # @example Enable resource based routing. + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client \ + # "my-instance", "my-database", enable_resource_based_routing: true + # + # db.transaction do |tx| + # results = tx.execute_query "SELECT * FROM users" + # + # results.rows.each do |row| + # puts "User #{row[:id]} is #{row[:name]}" + # end + # end + # + def client \ + instance_id, + database_id, + pool: {}, + labels: nil, + enable_resource_based_routing: false # Convert from possible Google::Protobuf::Map labels = Hash[labels.map { |k, v| [String(k), String(v)] }] if labels - Client.new self, instance_id, database_id, - session_labels: labels, - pool_opts: valid_session_pool_options(pool) + Client.new \ + self, + instance_id, + database_id, + session_labels: labels, + pool_opts: valid_session_pool_options(pool), + enable_resource_based_routing: enable_resource_based_routing end ## @@ -501,6 +539,11 @@ def client instance_id, database_id, pool: {}, labels: nil # * Label values must be between 0 and 63 characters long and must # conform to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. # * No more than 64 labels can be associated with a given resource. + # @param [Boolean] enable_resource_based_routing Enable/Disable + # resource-based routing for data operation, by default it + # is disabled. Resource based routing can be enabled using the + # environment `GOOGLE_CLOUD_SPANNER_ENABLE_RESOURCE_BASED_ROUTING` to + # `TRUE` or `true`. # # @return [Client] The newly created client. # @@ -529,10 +572,45 @@ def client instance_id, database_id, pool: {}, labels: nil # results = new_batch_snapshot.execute_partition \ # new_partition # - def batch_client instance_id, database_id, labels: nil + # @example Enable resource based routing. + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # batch_client = spanner.batch_client \ + # "my-instance", "my-database", enable_resource_based_routing: true + # + # batch_snapshot = batch_client.batch_snapshot + # serialized_snapshot = batch_snapshot.dump + # + # partitions = batch_snapshot.partition_read "users", [:id, :name] + # + # partition = partitions.first + # serialized_partition = partition.dump + # + # # In a separate process + # new_batch_snapshot = batch_client.load_batch_snapshot \ + # serialized_snapshot + # + # new_partition = batch_client.load_partition \ + # serialized_partition + # + # results = new_batch_snapshot.execute_partition \ + # new_partition + # + def batch_client \ + instance_id, + database_id, + labels: nil, + enable_resource_based_routing: false # Convert from possible Google::Protobuf::Map labels = Hash[labels.map { |k, v| [String(k), String(v)] }] if labels - BatchClient.new self, instance_id, database_id, session_labels: labels + BatchClient.new \ + self, + instance_id, + database_id, + session_labels: labels, + enable_resource_based_routing: enable_resource_based_routing end protected diff --git a/google-cloud-spanner/lib/google/cloud/spanner/resource_based_routing.rb b/google-cloud-spanner/lib/google/cloud/spanner/resource_based_routing.rb new file mode 100644 index 000000000000..2f09f8a5d4f3 --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/resource_based_routing.rb @@ -0,0 +1,63 @@ +# 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 +# +# https://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. + + +module Google + module Cloud + module Spanner + ## + # @private Helper module for resource based routing client. + module ResourceBasedRouting + # Check resource based routing is enabled or not. + # + # @return [Boolean] + def resource_based_routing_enabled? + return true if @enable_resource_based_routing + + ["TRUE", "true"].include? \ + ENV["GOOGLE_CLOUD_SPANNER_ENABLE_RESOURCE_BASED_ROUTING"] + end + + protected + + # Returns a Service that uses the first endpoint uri for the instance. + # + # @return [Spanner::Service, nil] Returns service instance if instance + # endpoint uris present. + def resource_based_routing_service + instance = @project.instance @instance_id, fields: ["endpoint_uris"] + return if instance.nil? || instance.endpoint_uris.empty? + + Spanner::Service.new \ + @project.project_id, + @project.service.credentials, + host: instance.endpoint_uris.first, + timeout: @project.service.timeout, + client_config: @project.service.client_config + rescue Google::Cloud::PermissionDeniedError + warn <<~WARN + The client library attempted to connect to an endpoint + closer to your Cloud Spanner data but was unable to do so. + The client library will fallback and route requests to the + endpoint given in the client options, which may result in + increased latency. We recommend including the scope + https://www.googleapis.com/auth/spanner.admin so that + the client library can get an instance-specific endpoint + and efficiently route requests. + WARN + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/service.rb b/google-cloud-spanner/lib/google/cloud/spanner/service.rb index 1f758e370b3d..acb69f76aee5 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/service.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/service.rb @@ -120,9 +120,11 @@ def list_instances token: nil, max: nil end end - def get_instance name + def get_instance name, fields: nil + field_mask = Google::Protobuf::FieldMask.new(paths: fields || []) + execute do - instances.get_instance instance_path(name) + instances.get_instance instance_path(name), field_mask: field_mask end end @@ -284,10 +286,11 @@ def batch_create_sessions database_name, session_count, labels: nil session = Google::Spanner::V1::Session.new labels: labels if labels execute do # The response may have fewer sessions than requested in the RPC. - service.batch_create_sessions database_name, - session_count, - session_template: session, - options: opts + service.batch_create_sessions \ + database_name, + session_count, + session_template: session, + options: opts end end @@ -318,11 +321,12 @@ def execute_batch_dml session_name, transaction, statements, seqno opts = default_options_from_session session_name statements = statements.map(&:to_grpc) results = execute do - service.execute_batch_dml session_name, - transaction, - statements, - seqno, - options: opts + service.execute_batch_dml \ + session_name, + transaction, + statements, + seqno, + options: opts end if results.status.code.zero? results.result_sets.map { |rs| rs.stats.row_count_exact } @@ -392,7 +396,8 @@ def commit session_name, mutations = [], transaction_id: nil execute do service.commit \ session_name, mutations, - transaction_id: transaction_id, single_use_transaction: tx_opts, + transaction_id: transaction_id, + single_use_transaction: tx_opts, options: opts end end @@ -439,7 +444,8 @@ def create_pdml session_name ) opts = default_options_from_session session_name execute do - service.begin_transaction session_name, tx_opts, options: opts + service.begin_transaction \ + session_name, tx_opts, options: opts end end diff --git a/google-cloud-spanner/support/doctest_helper.rb b/google-cloud-spanner/support/doctest_helper.rb index 5e02001bb434..bd62bb0c8729 100644 --- a/google-cloud-spanner/support/doctest_helper.rb +++ b/google-cloud-spanner/support/doctest_helper.rb @@ -108,14 +108,14 @@ def mock_spanner mock_client = Minitest::Mock.new mock_instances.expect :create_instance, create_instance_resp(client: mock_client), ["projects/my-project", "my-new-instance", Google::Spanner::Admin::Instance::V1::Instance] mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {:options=>nil}] - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-new-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-new-instance", Hash] end end doctest.before "Google::Cloud::Spanner::Instance#create_database" do mock_spanner do |mock, mock_instances, mock_databases| mock_client = Minitest::Mock.new - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {:options=>nil}] mock_databases.expect :create_database, create_database_resp(client: mock_client), ["projects/my-project/instances/my-instance", "CREATE DATABASE `my-new-database`", {:extra_statements=>[]}] end @@ -123,21 +123,21 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Instance#database" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-database"] end end doctest.before "Google::Cloud::Spanner::Instance#database@Will return `nil` if instance does not exist." do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_databases.expect :get_database, nil, ["projects/my-project/instances/my-instance/databases/my-database"] end end doctest.before "Google::Cloud::Spanner::Instance#databases" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_databases.expect :list_databases, databases_resp(token: "token"), ["projects/my-project/instances/my-instance", Hash] mock_databases.expect :list_databases, databases_resp, ["projects/my-project/instances/my-instance", Hash] end @@ -145,7 +145,7 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Instance#policy" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_instances.expect :get_iam_policy, policy_resp, ["projects/my-project/instances/my-instance"] mock_instances.expect :set_iam_policy, policy_resp, ["projects/my-project/instances/my-instance", Google::Iam::V1::Policy] end @@ -153,7 +153,7 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Instance#update_policy" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_instances.expect :get_iam_policy, policy_resp, ["projects/my-project/instances/my-instance"] mock_instances.expect :set_iam_policy, policy_resp, ["projects/my-project/instances/my-instance", Google::Iam::V1::Policy] end @@ -161,14 +161,14 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Instance#test_permissions" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_instances.expect :test_iam_permissions, test_permissions_res, ["projects/my-project/instances/my-instance", ["spanner.instances.get", "spanner.instances.update"]] end end doctest.before "Google::Cloud::Spanner::Instance#delete" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_instances.expect :delete_instance, nil, ["projects/my-project/instances/my-instance"] end end @@ -310,7 +310,7 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Policy" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_instances.expect :get_iam_policy, policy_resp, ["projects/my-project/instances/my-instance"] mock_instances.expect :set_iam_policy, policy_resp, ["projects/my-project/instances/my-instance", Google::Iam::V1::Policy] end @@ -320,7 +320,7 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Project" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-database"] end end @@ -347,6 +347,18 @@ def mock_spanner end end + doctest.before "Google::Cloud::Spanner::Project#batch_client@Enable resource based routing." do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] + mock.expect :create_session, session_grpc, ["projects/my-project/instances/my-instance/databases/my-database", Hash] + mock.expect :begin_transaction, tx_resp, ["session-name", Google::Spanner::V1::TransactionOptions, Hash] + mock.expect :partition_read, OpenStruct.new(partitions: [Google::Spanner::V1::Partition.new(partition_token: "partition-token")]), + ["session-name", "users", Google::Spanner::V1::KeySet, Hash] + mock.expect :streaming_read, results_enum, ["session-name", "users", ["id", "name"], Google::Spanner::V1::KeySet, Hash] + mock.expect :delete_session, session_grpc, ["session-name", Hash] + end + end + doctest.before "Google::Cloud::Spanner::Project#client" do mock_spanner do |mock, mock_instances, mock_databases| mock.expect :batch_create_sessions, OpenStruct.new(session: Array.new(10) { session_grpc }), ["projects/my-project/instances/my-instance/databases/my-database", 10, Hash] @@ -358,24 +370,36 @@ def mock_spanner end end + doctest.before "Google::Cloud::Spanner::Project#client@Enable resource based routing." do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] + mock.expect :batch_create_sessions, OpenStruct.new(session: Array.new(10) { session_grpc }), ["projects/my-project/instances/my-instance/databases/my-database", 10, Hash] + 5.times do + mock.expect :begin_transaction, tx_resp, ["session-name", Google::Spanner::V1::TransactionOptions, Hash] + end + mock.expect :execute_streaming_sql, results_enum, ["session-name", "SELECT * FROM users", Hash] + mock.expect :commit, commit_resp, ["session-name", Array, Hash] + end + end + doctest.before "Google::Cloud::Spanner::Project#create_instance" do mock_spanner do |mock, mock_instances, mock_databases| mock_client = Minitest::Mock.new mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {:options=>nil}] mock_instances.expect :create_instance, create_instance_resp(client: mock_client), ["projects/my-project", "my-new-instance", Google::Spanner::Admin::Instance::V1::Instance] - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-new-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-new-instance", Hash] end end doctest.before "Google::Cloud::Spanner::Project#instance" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] end end doctest.before "Google::Cloud::Spanner::Project#instance@Will return `nil` if instance does not exist." do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/non-existing"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/non-existing", Hash] end end @@ -416,7 +440,7 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Project#databases" do mock_spanner do |mock, mock_instances, mock_databases| - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_databases.expect :list_databases, databases_resp(token: "token"), ["projects/my-project/instances/my-instance", Hash] mock_databases.expect :list_databases, databases_resp, ["projects/my-project/instances/my-instance", Hash] end @@ -435,6 +459,18 @@ def mock_spanner end end + doctest.before "Google::Cloud::Spanner::Client@Enable resource based routing." do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] + mock.expect :batch_create_sessions, OpenStruct.new(session: Array.new(10) { session_grpc }), ["projects/my-project/instances/my-instance/databases/my-database", 10, Hash] + 5.times do + mock.expect :begin_transaction, tx_resp, ["session-name", Google::Spanner::V1::TransactionOptions, Hash] + end + mock.expect :execute_streaming_sql, results_enum, ["session-name", "SELECT * FROM users", Hash] + mock.expect :commit, commit_resp, ["session-name", Array, Hash] + end + end + doctest.before "Google::Cloud::Spanner::Client#execute" do mock_spanner do |mock, mock_instances, mock_databases| mock.expect :batch_create_sessions, OpenStruct.new(session: Array.new(10) { session_grpc }), ["projects/my-project/instances/my-instance/databases/my-database", 10, Hash] @@ -550,7 +586,7 @@ def mock_spanner doctest.before "Google::Cloud::Spanner::Database" do mock_spanner do |mock, mock_instances, mock_databases| mock_client = Minitest::Mock.new - mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance", Hash] mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {:options=>nil}] mock_databases.expect :create_database, create_database_resp(client: mock_client), ["projects/my-project/instances/my-instance", "CREATE DATABASE `my-new-database`", {:extra_statements=>[]}] mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-new-database"] @@ -826,17 +862,6 @@ def project "my-project" end -def instance_hash name: "my-instance", nodes: 1, state: "READY", labels: {} - { - name: "projects/#{project}/instances/#{name}", - config: "projects/#{project}/instanceConfigs/regional-us-central1", - display_name: name.split("-").map(&:capitalize).join(" "), - nodeCount: nodes, - state: state, - labels: labels - } -end - def job_grpc Google::Longrunning::Operation.new( name: "1234567890", @@ -895,7 +920,8 @@ def instance_hash name: "my-instance", nodes: 1, state: "READY", labels: {} display_name: name.split("-").map(&:capitalize).join(" "), node_count: nodes, state: state, - labels: labels + labels: labels, + endpoint_uris: [] } end diff --git a/google-cloud-spanner/test/google/cloud/spanner/batch_client_test.rb b/google-cloud-spanner/test/google/cloud/spanner/batch_client_test.rb index 37c636cbd971..30b25805839b 100644 --- a/google-cloud-spanner/test/google/cloud/spanner/batch_client_test.rb +++ b/google-cloud-spanner/test/google/cloud/spanner/batch_client_test.rb @@ -48,7 +48,7 @@ it "retrieves the instance" do get_res = Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) mock = Minitest::Mock.new - mock.expect :get_instance, get_res, [instance_path(instance_id)] + mock.expect :get_instance, get_res, [instance_path(instance_id), Hash] spanner.service.mocked_instances = mock instance = spanner.instance instance_id diff --git a/google-cloud-spanner/test/google/cloud/spanner/client/admin_test.rb b/google-cloud-spanner/test/google/cloud/spanner/client/admin_test.rb index 0ca0691cf5cb..9222e71eda41 100644 --- a/google-cloud-spanner/test/google/cloud/spanner/client/admin_test.rb +++ b/google-cloud-spanner/test/google/cloud/spanner/client/admin_test.rb @@ -42,7 +42,7 @@ it "retrieves the instance" do get_res = Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) mock = Minitest::Mock.new - mock.expect :get_instance, get_res, [instance_path(instance_id)] + mock.expect :get_instance, get_res, [instance_path(instance_id), Hash] spanner.service.mocked_instances = mock instance = spanner.instance instance_id diff --git a/google-cloud-spanner/test/google/cloud/spanner/client/resource_based_routing_test.rb b/google-cloud-spanner/test/google/cloud/spanner/client/resource_based_routing_test.rb new file mode 100644 index 000000000000..585b8f26a243 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/client/resource_based_routing_test.rb @@ -0,0 +1,137 @@ +# 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 +# +# https://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. + +require "helper" + +describe Google::Cloud::Spanner::Client, :resource_based_routing, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:session_id) { "session123" } + let(:session_grpc) { Google::Spanner::V1::Session.new name: session_path(instance_id, database_id, session_id) } + let(:session) { Google::Cloud::Spanner::Session.from_grpc session_grpc, spanner.service } + let(:pool_opts) { { min: 0, max: 4 } } + + before do + session.instance_variable_set :@last_updated_at, Time.now + ENV.delete "GOOGLE_CLOUD_SPANNER_ENABLE_RESOURCE_BASED_ROUTING" + end + + after do + ENV.delete "GOOGLE_CLOUD_SPANNER_ENABLE_RESOURCE_BASED_ROUTING" + end + + + it "sets service with default host" do + client = Google::Cloud::Spanner::Client.new \ + spanner, instance_id, database_id, pool_opts: { min: 0, max: 4 } + + client.service.host.must_equal \ + Google::Cloud::Spanner::V1::SpannerClient::SERVICE_ADDRESS + client.resource_based_routing_enabled?.must_equal false + end + + it "sets service host to default service uri if resource based routing disabled" do + client = Google::Cloud::Spanner::Client.new \ + spanner, instance_id, database_id, pool_opts: { min: 0, max: 4 }, + enable_resource_based_routing: false + + client.service.host.must_equal \ + Google::Cloud::Spanner::V1::SpannerClient::SERVICE_ADDRESS + client.resource_based_routing_enabled?.must_equal false + end + + it "set service host to instance endpoint uri if resource based routing enabled" do + get_res = Google::Spanner::Admin::Instance::V1::Instance.new \ + name: instance_path(instance_id), endpoint_uris: ["test1.host.com", "test2.host.com"] + mock = Minitest::Mock.new + mock.expect :get_instance, get_res, [ + instance_path(instance_id), + field_mask: Google::Protobuf::FieldMask.new(paths: ["endpoint_uris"]) + ] + spanner.service.mocked_instances = mock + + Google::Cloud::Spanner::Pool.stub :new, Object.new do + client = Google::Cloud::Spanner::Client.new \ + spanner, instance_id, database_id, enable_resource_based_routing: true + client.resource_based_routing_enabled?.must_equal true + client.service.host.must_equal "test1.host.com" + end + + mock.verify + end + + it "set service host with instance endpoint uri if resource based routing enabled using an environment variable" do + ENV["GOOGLE_CLOUD_SPANNER_ENABLE_RESOURCE_BASED_ROUTING"] = "TRUE" + + get_res = Google::Spanner::Admin::Instance::V1::Instance.new \ + name: instance_path(instance_id), endpoint_uris: ["test1.host.com", "test2.host.com"] + mock = Minitest::Mock.new + mock.expect :get_instance, get_res, [ + instance_path(instance_id), + field_mask: Google::Protobuf::FieldMask.new(paths: ["endpoint_uris"]) + ] + spanner.service.mocked_instances = mock + + Google::Cloud::Spanner::Pool.stub :new, Object.new do + client = Google::Cloud::Spanner::Client.new \ + spanner, instance_id, database_id, enable_resource_based_routing: true + client.resource_based_routing_enabled?.must_equal true + client.service.host.must_equal "test1.host.com" + end + + mock.verify + end + + it "set default endpoint uri if resource based routing enabled and instance endpoint uris not present" do + get_res = Google::Spanner::Admin::Instance::V1::Instance.new \ + name: instance_path(instance_id), endpoint_uris: [] + mock = Minitest::Mock.new + mock.expect :get_instance, get_res, [ + instance_path(instance_id), + field_mask: Google::Protobuf::FieldMask.new(paths: ["endpoint_uris"]) + ] + spanner.service.mocked_instances = mock + + Google::Cloud::Spanner::Pool.stub :new, Object.new do + client = Google::Cloud::Spanner::Client.new \ + spanner, instance_id, database_id, enable_resource_based_routing: true + client.service.host.must_equal \ + Google::Cloud::Spanner::V1::SpannerClient::SERVICE_ADDRESS + client.resource_based_routing_enabled?.must_equal true + end + + mock.verify + end + + it "returns default endpoint uri if get instance permission denied" do + stub = OpenStruct.new(api_call_count: 0) + + def stub.get_instance *args + self.api_call_count += 1 + raise Google::Cloud::Error.from_error( + GRPC::PermissionDenied.new "permission denied" + ) + end + spanner.service.mocked_instances = stub + + Google::Cloud::Spanner::Pool.stub :new, Object.new do + client = Google::Cloud::Spanner::Client.new \ + spanner, instance_id, database_id, enable_resource_based_routing: true + client.service.host.must_equal \ + Google::Cloud::Spanner::V1::SpannerClient::SERVICE_ADDRESS + stub.api_call_count.must_equal 1 + client.resource_based_routing_enabled?.must_equal true + end + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/instance_test.rb b/google-cloud-spanner/test/google/cloud/spanner/instance_test.rb index e9054a712605..c4e95b6df898 100644 --- a/google-cloud-spanner/test/google/cloud/spanner/instance_test.rb +++ b/google-cloud-spanner/test/google/cloud/spanner/instance_test.rb @@ -27,5 +27,6 @@ instance.state.must_equal :READY instance.must_be :ready? instance.wont_be :creating? + instance.endpoint_uris.must_be_empty end end diff --git a/google-cloud-spanner/test/google/cloud/spanner/project/instance_test.rb b/google-cloud-spanner/test/google/cloud/spanner/project/instance_test.rb index 73bd60dda43e..39fd4790ac3d 100644 --- a/google-cloud-spanner/test/google/cloud/spanner/project/instance_test.rb +++ b/google-cloud-spanner/test/google/cloud/spanner/project/instance_test.rb @@ -20,7 +20,7 @@ get_res = Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) mock = Minitest::Mock.new - mock.expect :get_instance, get_res, [instance_path(instance_id)] + mock.expect :get_instance, get_res, [instance_path(instance_id), Hash] spanner.service.mocked_instances = mock instance = spanner.instance instance_id @@ -48,4 +48,29 @@ def stub.get_instance *args instance = spanner.instance not_found_instance_id instance.must_be :nil? end + + it "returns instance with provided fields" do + instance_id = "my-instance-id" + + get_res = Google::Spanner::Admin::Instance::V1::Instance.new \ + name: instance_path(instance_id), endpoint_uris: ["test.host.com"] + mock = Minitest::Mock.new + mock.expect :get_instance, get_res, [ + instance_path(instance_id), + field_mask: Google::Protobuf::FieldMask.new(paths: ["name", "endpoint_uris"]) + ] + spanner.service.mocked_instances = mock + + instance = spanner.instance instance_id, fields: ["name", "endpoint_uris"] + + mock.verify + + instance.project_id.must_equal project + instance.instance_id.must_equal instance_id + instance.path.must_equal instance_path(instance_id) + instance.endpoint_uris.must_equal ["test.host.com"] + instance.name.must_equal "" + instance.node_count.must_equal 0 + instance.state.must_equal :STATE_UNSPECIFIED + end end