diff --git a/google-cloud-spanner/lib/google/cloud/spanner/client.rb b/google-cloud-spanner/lib/google/cloud/spanner/client.rb index f39c009..6c4a107 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/client.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/client.rb @@ -2227,11 +2227,11 @@ def transaction deadline: 120, exclude_txn_from_change_streams: false, request_options, tag_type: :transaction_tag @pool.with_session do |session| + transaction_tag = request_options[:transaction_tag] if request_options tx = session.create_empty_transaction \ - exclude_txn_from_change_streams: exclude_txn_from_change_streams, read_lock_mode: read_lock_mode - if request_options - tx.transaction_tag = request_options[:transaction_tag] - end + exclude_txn_from_change_streams: exclude_txn_from_change_streams, + read_lock_mode: read_lock_mode, + transaction_tag: transaction_tag begin Thread.current[IS_TRANSACTION_RUNNING_KEY] = true @@ -2289,11 +2289,9 @@ def transaction deadline: 120, exclude_txn_from_change_streams: false, tx = session.create_empty_transaction( exclude_txn_from_change_streams: exclude_txn_from_change_streams, previous_transaction_id: previous_transaction_id, - read_lock_mode: read_lock_mode + read_lock_mode: read_lock_mode, + transaction_tag: transaction_tag ) - if request_options - tx.transaction_tag = request_options[:transaction_tag] - end retry rescue StandardError => e # Rollback transaction when handling unexpected error diff --git a/google-cloud-spanner/lib/google/cloud/spanner/session.rb b/google-cloud-spanner/lib/google/cloud/spanner/session.rb index 74c9cc0..d1a1557 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/session.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/session.rb @@ -1488,17 +1488,24 @@ def rollback transaction_id # @param exclude_txn_from_change_streams [::Boolean] Optional. Defaults to `false`. # When `exclude_txn_from_change_streams` is set to `true`, it prevents read # or write transactions from being tracked in change streams. + # @param request_options [::Hash, nil] Optional. Common request options. + # Example option: `:priority`. # @private # @return [::Google::Cloud::Spanner::Transaction] - def create_transaction exclude_txn_from_change_streams: false, read_lock_mode: nil + def create_transaction exclude_txn_from_change_streams: false, read_lock_mode: nil, + request_options: nil route_to_leader = LARHeaders.begin_transaction true + request_options = Convert.to_request_options request_options, tag_type: :transaction_tag + transaction_tag = request_options[:transaction_tag] if request_options tx_grpc = service.begin_transaction path, route_to_leader: route_to_leader, exclude_txn_from_change_streams: exclude_txn_from_change_streams, + request_options: request_options, read_lock_mode: read_lock_mode Transaction.from_grpc \ tx_grpc, self, - exclude_txn_from_change_streams: exclude_txn_from_change_streams, read_lock_mode: read_lock_mode + exclude_txn_from_change_streams: exclude_txn_from_change_streams, read_lock_mode: read_lock_mode, + transaction_tag: transaction_tag end # Creates a new empty transaction wrapper without a server-side object. @@ -1512,12 +1519,15 @@ def create_transaction exclude_txn_from_change_streams: false, read_lock_mode: n # An id of the previous transaction, if this new transaction wrapper is being created # as a part of a retry. Previous transaction id should be added to TransactionOptions # of a new ReadWrite transaction when retry is attempted. + # @param transaction_tag [::String, nil] Optional. + # A tag used for statistics collection about this transaction. # @private # @return [::Google::Cloud::Spanner::Transaction] The new *empty-wrapper* transaction object. def create_empty_transaction exclude_txn_from_change_streams: false, previous_transaction_id: nil, - read_lock_mode: nil + read_lock_mode: nil, transaction_tag: nil Transaction.from_grpc nil, self, exclude_txn_from_change_streams: exclude_txn_from_change_streams, -previous_transaction_id: previous_transaction_id, read_lock_mode: read_lock_mode + previous_transaction_id: previous_transaction_id, read_lock_mode: read_lock_mode, + transaction_tag: transaction_tag end # If the session is non-multiplexed, keeps the session alive by executing `"SELECT 1"`. diff --git a/google-cloud-spanner/lib/google/cloud/spanner/transaction.rb b/google-cloud-spanner/lib/google/cloud/spanner/transaction.rb index 75518a1..c1b219d 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/transaction.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/transaction.rb @@ -124,13 +124,17 @@ class Transaction # An id of the previous transaction, if this new transaction wrapper is being created # as a part of a retry. Previous transaction id should be added to TransactionOptions # of a new ReadWrite transaction when retry is attempted. + # @param transaction_tag [::String, nil] Optional. + # A tag used for statistics collection about this transaction. # @private # @return [::Google::Cloud::Spanner::Transaction] - def initialize grpc, session, exclude_txn_from_change_streams, previous_transaction_id: nil, read_lock_mode: nil + def initialize grpc, session, exclude_txn_from_change_streams, previous_transaction_id: nil, + read_lock_mode: nil, transaction_tag: nil @grpc = grpc @session = session @exclude_txn_from_change_streams = exclude_txn_from_change_streams @read_lock_mode = read_lock_mode + @transaction_tag = transaction_tag # throwing away empty strings for simplicity unless previous_transaction_id.nil? || previous_transaction_id.empty? @@ -1253,12 +1257,14 @@ def mutations # An id of the previous transaction, if this new transaction wrapper is being created # as a part of a retry. Previous transaction id should be added to TransactionOptions # of a new ReadWrite transaction when retry is attempted. + # @param transaction_tag [::String, nil] Optional. + # A tag used for statistics collection about this transaction. # @private # @return [::Google::Cloud::Spanner::Transaction] def self.from_grpc grpc, session, exclude_txn_from_change_streams: false, previous_transaction_id: nil, - read_lock_mode: nil + read_lock_mode: nil, transaction_tag: nil new grpc, session, exclude_txn_from_change_streams, previous_transaction_id: previous_transaction_id, -read_lock_mode: read_lock_mode + read_lock_mode: read_lock_mode, transaction_tag: transaction_tag end ## @@ -1297,6 +1303,7 @@ def safe_begin_transaction! exclude_from_change_streams: false, request_options: return if existing_transaction? ensure_session! route_to_leader = LARHeaders.begin_transaction true + request_options = build_request_options request_options # TODO: [virost@, 2025-10] fix this so it uses tx_selector # instead of re-creating it within `Service#begin_transaction` @@ -1377,7 +1384,7 @@ def tx_selector exclude_txn_from_change_streams: false, read_lock_mode: nil ## # @private Build request options. If transaction tag is set - # then add into request options. + # then add it to the request options. def build_request_options options options = Convert.to_request_options options, tag_type: :request_tag diff --git a/google-cloud-spanner/test/google/cloud/spanner/client/transaction_tag_test.rb b/google-cloud-spanner/test/google/cloud/spanner/client/transaction_tag_test.rb new file mode 100644 index 0000000..dbe2373 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/client/transaction_tag_test.rb @@ -0,0 +1,99 @@ +# Copyright 2026 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, :transaction, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:session_id) { "session123" } + let(:session_grpc) { Google::Cloud::Spanner::V1::Session.new name: session_path(instance_id, database_id, session_id), multiplexed: true } + let(:session) { Google::Cloud::Spanner::Session.from_grpc session_grpc, spanner.service } + let(:transaction_id) { "tx789" } + let(:transaction_grpc) { Google::Cloud::Spanner::V1::Transaction.new id: transaction_id } + let(:default_options) { ::Gapic::CallOptions.new metadata: { "google-cloud-resource-prefix" => database_path(instance_id, database_id) } } + let(:client) { spanner.client instance_id, database_id } + let(:tx_opts) { Google::Cloud::Spanner::V1::TransactionOptions.new(read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new) } + let(:commit_time) { Time.now } + let(:commit_timestamp) { Google::Cloud::Spanner::Convert.time_to_timestamp commit_time } + let(:commit_resp) { Google::Cloud::Spanner::V1::CommitResponse.new commit_timestamp: commit_timestamp } + + it "passes transaction tag to BeginTransaction when transaction_id is called lazily" do + mock = Minitest::Mock.new + spanner.service.mocked_service = mock + + mock.expect :create_session, session_grpc, [{ database: database_path(instance_id, database_id), session: default_session_request }, default_options] + + # This is the call we are testing. It should have the transaction_tag. + expected_tx_opts = Google::Cloud::Spanner::V1::TransactionOptions.new( + read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new( + read_lock_mode: :READ_LOCK_MODE_UNSPECIFIED, + multiplexed_session_previous_transaction_id: "" + ), + exclude_txn_from_change_streams: false, + isolation_level: :ISOLATION_LEVEL_UNSPECIFIED + ) + + mock.expect :begin_transaction, transaction_grpc, [{ + session: session_grpc.name, + options: expected_tx_opts, + request_options: { transaction_tag: "Tag-1" }, + mutation_key: nil + }, default_options] + + mock.expect :commit, commit_resp, [{ + session: session_grpc.name, + mutations: [], + transaction_id: transaction_id, + single_use_transaction: nil, + request_options: { transaction_tag: "Tag-1" }, + precommit_token: nil + }, default_options] + + client.transaction request_options: { tag: "Tag-1" } do |tx| + # Calling transaction_id triggers safe_begin_transaction! + id = tx.transaction_id + _(id).must_equal transaction_id + end + + mock.verify + end + + it "passes transaction tag when calling Session#create_transaction explicitly" do + mock = Minitest::Mock.new + spanner.service.mocked_service = mock + + expected_tx_opts = Google::Cloud::Spanner::V1::TransactionOptions.new( + read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new( + read_lock_mode: :READ_LOCK_MODE_UNSPECIFIED, + multiplexed_session_previous_transaction_id: "" + ), + exclude_txn_from_change_streams: false, + isolation_level: :ISOLATION_LEVEL_UNSPECIFIED + ) + + mock.expect :begin_transaction, transaction_grpc, [{ + session: session_grpc.name, + options: expected_tx_opts, + request_options: { transaction_tag: "Tag-1" }, + mutation_key: nil + }, default_options] + + tx = session.create_transaction request_options: { tag: "Tag-1" } + _(tx).must_be_kind_of Google::Cloud::Spanner::Transaction + _(tx.transaction_id).must_equal transaction_id + + mock.verify + end +end