Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions google-cloud-spanner/lib/google/cloud/spanner/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions google-cloud-spanner/lib/google/cloud/spanner/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"`.
Expand Down
15 changes: 11 additions & 4 deletions google-cloud-spanner/lib/google/cloud/spanner/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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

##
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading