-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add transaction_connection_validator extension for retrying transacti…
…ons on new connection if there is a disconnect error when starting transaction This is a less invasive approach than the connection_validator extension, while still having mostly the same effect. It's limited to recognizing disconnect errors when starting transactions, but for many applications, those are the most important cases where retrying is useful.
- Loading branch information
1 parent
ad4b962
commit c31dde2
Showing
6 changed files
with
199 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# frozen-string-literal: true | ||
# | ||
# The transaction_connection_validator extension automatically | ||
# retries a transaction on a connection if an disconnect error | ||
# is raised when sending the statement to begin a new | ||
# transaction, as long as the user has not already checked out | ||
# a connection. This is safe to do because no other queries | ||
# have been issued on the connection, and no user-level code | ||
# is run before retrying. | ||
# | ||
# This approach to connection validation can be significantly | ||
# lower overhead than the connection_validator extension, | ||
# though it does not handle all cases handled by the | ||
# connection_validator extension. However, it performs the | ||
# validation checks on every new transaction, so it will | ||
# automatically handle disconnected connections in some cases | ||
# where the connection_validator extension will not by default | ||
# (as the connection_validator extension only checks | ||
# connections if they have not been used in the last hour by | ||
# default). | ||
# | ||
# Related module: Sequel::TransactionConnectionValidator | ||
|
||
# | ||
module Sequel | ||
module TransactionConnectionValidator | ||
class DisconnectRetry < DatabaseDisconnectError | ||
# The connection that raised the disconnect error | ||
attr_accessor :connection | ||
|
||
# The underlying disconnect error, in case it needs to be reraised. | ||
attr_accessor :database_error | ||
end | ||
|
||
# Rescue disconnect errors raised when beginning a new transaction. If there | ||
# is a disconnnect error, it should be safe to retry the transaction using a | ||
# new connection, as we haven't yielded control to the user yet. | ||
def transaction(opts=OPTS) | ||
super | ||
rescue DisconnectRetry => e | ||
if synchronize(opts[:server]){|conn| conn.equal?(e.connection)} | ||
# If retrying would use the same connection, that means the | ||
# connection was not removed from the pool, which means the caller has | ||
# already checked out the connection, and retrying will not be successful. | ||
# In this case, we can only reraise the exception. | ||
raise e.database_error | ||
end | ||
|
||
num_retries ||= 0 | ||
num_retries += 1 | ||
retry if num_retries < 5 | ||
|
||
raise e.database_error | ||
end | ||
|
||
private | ||
|
||
# Reraise disconnect errors as DisconnectRetry so they can be retried. | ||
def begin_new_transaction(conn, opts) | ||
super | ||
rescue Sequel::DatabaseDisconnectError, *database_error_classes => e | ||
if e.is_a?(Sequel::DatabaseDisconnectError) || disconnect_error?(e, OPTS) | ||
exception = DisconnectRetry.new(e.message) | ||
exception.set_backtrace([]) | ||
exception.connection = conn | ||
unless e.is_a?(Sequel::DatabaseError) | ||
e = Sequel.convert_exception_class(e, database_error_class(e, OPTS)) | ||
end | ||
exception.database_error = e | ||
raise exception | ||
end | ||
|
||
raise | ||
end | ||
end | ||
|
||
Database.register_extension(:transaction_connection_validator, TransactionConnectionValidator) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
spec/extensions/transaction_connection_validator_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
require_relative "spec_helper" | ||
|
||
describe "transaction_connection_validator extension" do | ||
database_error = Class.new(StandardError) | ||
|
||
before do | ||
@db = Sequel.mock | ||
@m = Module.new do | ||
def post_execute(conn, sql); end | ||
def disconnect_connection(conn) | ||
@sqls << 'disconnect' | ||
end | ||
define_method(:database_error_classes) do | ||
[database_error] | ||
end | ||
def disconnect_error?(e, opts) | ||
e.message.include? 'disconnect' | ||
end | ||
def connect(server) | ||
@sqls << 'connect' | ||
super | ||
end | ||
def log_connection_execute(conn, sql) | ||
res = super | ||
post_execute(conn, sql) | ||
res | ||
end | ||
end | ||
@db.extend @m | ||
@db.extension(:transaction_connection_validator) | ||
end | ||
|
||
it "should not affect transactions that do not raise exceptions" do | ||
@db.transaction{} | ||
@db.sqls.must_equal ['BEGIN', 'COMMIT'] | ||
end | ||
|
||
it "should retry transactions for disconnects during BEGIN" do | ||
conns = [] | ||
@db.define_singleton_method(:post_execute) do |conn, sql| | ||
conns << conn | ||
raise database_error, "disconnect error" if @sqls == ['BEGIN'] | ||
end | ||
@db.transaction{} | ||
@db.sqls.must_equal ['BEGIN', 'ROLLBACK', 'disconnect', 'connect', 'BEGIN', 'COMMIT'] | ||
conns.uniq.size.must_equal 2 | ||
end | ||
|
||
it "should handle DatabaseDisconnectErrors as disconnects" do | ||
conns = [] | ||
@db.define_singleton_method(:post_execute) do |conn, sql| | ||
conns << conn | ||
raise Sequel::DatabaseDisconnectError if @sqls == ['BEGIN'] | ||
end | ||
@db.transaction{} | ||
@db.sqls.must_equal ['BEGIN', 'ROLLBACK', 'disconnect', 'connect', 'BEGIN', 'COMMIT'] | ||
conns.uniq.size.must_equal 2 | ||
end | ||
|
||
it "should not retry if a connection has already been checked out before calling transaction" do | ||
conns = [] | ||
@db.define_singleton_method(:post_execute) do |conn, sql| | ||
conns << conn | ||
raise Sequel::DatabaseDisconnectError if @sqls == ['BEGIN'] | ||
end | ||
|
||
c = nil | ||
proc do | ||
@db.synchronize do |c1| | ||
c = c1 | ||
@db.transaction{} | ||
end | ||
end.must_raise(Sequel::DatabaseDisconnectError) | ||
@db.sqls.must_equal ['BEGIN', 'ROLLBACK', 'disconnect'] | ||
conns.uniq.must_equal [c] | ||
end | ||
|
||
it "should not retry transaction setup more than 5 times" do | ||
conns = [] | ||
@db.define_singleton_method(:post_execute) do |conn, sql| | ||
conns << conn | ||
raise database_error, "disconnect error" | ||
end | ||
proc do | ||
@db.transaction{} | ||
end.must_raise(Sequel::DatabaseDisconnectError) | ||
@db.sqls.must_equal(['BEGIN', 'ROLLBACK', 'disconnect', 'connect'] * 5) | ||
conns.uniq.size.must_equal 5 | ||
end | ||
|
||
it "should not retry on non-disconnect errors" do | ||
conns = [] | ||
@db.define_singleton_method(:post_execute) do |conn, sql| | ||
conns << conn | ||
raise database_error, "normal error" if @sqls == ['BEGIN'] | ||
end | ||
|
||
proc{@db.transaction{}}.must_raise(Sequel::DatabaseError) | ||
@db.sqls.must_equal ['BEGIN', 'ROLLBACK'] | ||
conns.uniq.size.must_equal 1 | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters