Skip to content

Commit

Permalink
Merge pull request googleapis#1452 from blowmage/spanner-transaction-…
Browse files Browse the repository at this point in the history
…rerty-info

Spanner Transaction Improvements
  • Loading branch information
quartzmo authored May 8, 2017
2 parents 88dc81b + 91f6a72 commit c8cf022
Show file tree
Hide file tree
Showing 12 changed files with 560 additions and 17 deletions.
2 changes: 2 additions & 0 deletions google-cloud-spanner/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ Style/TrivialAccessors:
Enabled: false
Style/FileName:
Enabled: false # for lib/google-cloud-spanner.rb file
Lint/HandleExceptions:
Enabled: false
72 changes: 62 additions & 10 deletions google-cloud-spanner/lib/google/cloud/spanner/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.


require "google/cloud/errors"
require "google/cloud/spanner/errors"
require "google/cloud/spanner/project"
require "google/cloud/spanner/pool"
require "google/cloud/spanner/session"
Expand Down Expand Up @@ -446,6 +446,9 @@ def delete table, *id
# a single logical point in time across columns, rows, and tables in a
# database.
#
# @param [Numeric] deadline The total amount of time in seconds the
# transaction has to succeed.
#
# @yield [transaction] The block for reading and writing data.
# @yieldparam [Google::Cloud::Spanner::Transaction] transaction The
# Transaction object.
Expand All @@ -464,19 +467,32 @@ def delete table, *id
# end
# end
#
def transaction &block
def transaction deadline: 120, &block
ensure_service!

deadline = validate_deadline deadline
backoff = 1.0
start_time = current_time

@pool.with_session do |session|
tx_grpc = @project.service.begin_transaction session.path
tx = Transaction.from_grpc(tx_grpc, session)
tx = create_transaction! session
begin
block.call tx
rescue Google::Cloud::AbortedError
# TODO: retrieve delay from ABORTED error
# Retry the entire transaction
tx2_grpc = @project.service.begin_transaction session.path
tx2 = Transaction.from_grpc(tx2_grpc, session)
block.call tx2
rescue Google::Cloud::AbortedError => err
# Re-raise if deadline has passed
raise err if current_time - start_time > deadline
# Sleep the amount from RetryDelay, or incremental backoff
sleep(delay_from_aborted(err) || backoff *= 1.3)
# Create new transaction and retry the block
tx = create_transaction! session
retry
rescue RollbackError
# Transaction block was interrupted, allow to continue
rescue => err
# Rollback transaction when handling unexpeced error
# Don't use Transaction#rollback since it raises
session.rollback tx.send(:transaction_id)
raise err
end
end
nil
Expand Down Expand Up @@ -568,6 +584,11 @@ def ensure_service!
fail "Must have active connection to service" unless @project.service
end

def create_transaction! session
tx_grpc = @project.service.begin_transaction session.path
Transaction.from_grpc(tx_grpc, session)
end

##
# Check for valid snapshot arguments
def validate_single_use_args! timestamp: nil, staleness: nil
Expand Down Expand Up @@ -599,6 +620,37 @@ def validate_snapshot_args! strong: nil, timestamp: nil, staleness: nil
"Can only provide one of the following arguments: " \
"(strong, timestamp, staleness)"
end

def validate_deadline deadline
return 120 unless deadline.is_a? Numeric
return 120 if deadline < 0
deadline
end

##
# Defer to this method so we have something to mock for tests
def current_time
Time.now
end

##
# Retrieves the delay value from Google::Cloud::AbortedError
def delay_from_aborted err
return nil if err.nil?
if err.respond_to?(:metadata) && err.metadata["retryDelay"]
# a correct metadata will look like this:
# "{\"retryDelay\":{\"seconds\":60}}"
seconds = err.metadata["retryDelay"]["seconds"].to_i
nanos = err.metadata["retryDelay"]["nanos"].to_i
return seconds if nanos.zero?
return seconds + (nanos / 1000000000.0)
end
# No metadata? Try the inner error
delay_from_aborted(err.cause)
rescue
# Any error indicates the backoff should be handled elsewhere
return nil
end
end
end
end
Expand Down
29 changes: 29 additions & 0 deletions google-cloud-spanner/lib/google/cloud/spanner/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2017 Google Inc. All rights reserved.
#
# 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.


require "google/cloud/errors"

module Google
module Cloud
module Spanner
##
# # TransactionError
#
# General error for Transaction problems.
class RollbackError < Google::Cloud::Error
end
end
end
end
2 changes: 1 addition & 1 deletion google-cloud-spanner/lib/google/cloud/spanner/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.


require "google/cloud/errors"
require "google/cloud/spanner/errors"

module Google
module Cloud
Expand Down
2 changes: 1 addition & 1 deletion google-cloud-spanner/lib/google/cloud/spanner/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.


require "google/cloud/errors"
require "google/cloud/spanner/errors"
require "google/cloud/env"
require "google/cloud/spanner/service"
require "google/cloud/spanner/client"
Expand Down
2 changes: 1 addition & 1 deletion google-cloud-spanner/lib/google/cloud/spanner/results.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


require "google/cloud/spanner/convert"
require "google/cloud/errors"
require "google/cloud/spanner/errors"

module Google
module Cloud
Expand Down
9 changes: 8 additions & 1 deletion google-cloud-spanner/lib/google/cloud/spanner/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.


require "google/cloud/errors"
require "google/cloud/spanner/errors"
require "google/cloud/spanner/credentials"
require "google/cloud/spanner/version"
require "google/cloud/spanner/v1"
Expand Down Expand Up @@ -362,6 +362,13 @@ def commit session_name, mutations = [], transaction_id: nil
end
end

def rollback session_name, transaction_id
opts = default_options_from_session session_name
execute do
service.rollback session_name, transaction_id, options: opts
end
end

def begin_transaction session_name
tx_opts = Google::Spanner::V1::TransactionOptions.new(read_write:
Google::Spanner::V1::TransactionOptions::ReadWrite.new)
Expand Down
6 changes: 6 additions & 0 deletions google-cloud-spanner/lib/google/cloud/spanner/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@ def delete table, *id, transaction_id: nil
service.commit path, commit.mutations, transaction_id: transaction_id
end

##
# Rolls back the transaction, releasing any locks it holds.
def rollback transaction_id
service.rollback path, transaction_id
end

##
# @private
# Keeps the session alive by calling SELECT 1
Expand Down
21 changes: 21 additions & 0 deletions google-cloud-spanner/lib/google/cloud/spanner/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.


require "google/cloud/spanner/errors"
require "google/cloud/spanner/results"
require "google/cloud/spanner/commit"

Expand Down Expand Up @@ -356,6 +357,26 @@ def delete table, *id
session.delete table, id, transaction_id: transaction_id
end

##
# Rolls back the transaction, releasing any locks it holds. It is a good
# idea to call this for any transaction that includes one or more `read`
# or `execute` requests and ultimately decides not to commit.
#
# @example
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# db = spanner.client "my-instance", "my-database"
#
# db.transaction { |tx| tx.rollback }
#
def rollback
ensure_session!
session.rollback transaction_id
# Raise RollbackError so the client can stop the transaction.
fail RollbackError
end

##
# @private Creates a new Transaction instance from a
# Google::Spanner::V1::Transaction.
Expand Down
Loading

0 comments on commit c8cf022

Please sign in to comment.