Skip to content

Commit b07f638

Browse files
committed
Refactor dead lock victim retry code.
* Put custom #transaction method into core ext module. * Our #transaction method does not bind &block but yields in anonymous block. * Organize connection tests with context block at bottom.
1 parent 070787f commit b07f638

File tree

7 files changed

+201
-164
lines changed

7 files changed

+201
-164
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
* master *
33

4+
* New deadlock victim retry using the #retry_deadlock_victim config. [Joe Rafaniello]
5+
46
* Renamed #with_auto_reconnect to #with_sqlserver_error_handling now that it handles both dropped
57
connections and deadlock victim errors. Fixes #150 [Joe Rafaniello]
68

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The SQL Server adapter for ActiveRecord. If you need the adapter for SQL Server
66

77
## What's New
88

9+
* Deadlock victim retry logic using the #retry_deadlock_victim config.
910
* Proper interface to configure the connection and TinyTDS app name reported to SQL Server.
1011
* Rails 3.1 prepared statement support leverages cached query plans.
1112
If you use DBLIB/TinyTDS, you must use FreeTDS 0.91 !!!!!
@@ -111,6 +112,15 @@ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_string_database_type =
111112
It is important to remember that unicode types in SQL Server have approximately half the storage capacity as their counter parts. So where a normal string would max out at (8000) a unicode string will top off at (4000).
112113

113114

115+
#### Deadlock Victim Retry
116+
117+
In a config initializer, you can configure the adapter to retry deadlock victims' SQL. Note, this relies on us copying ActiveRecord's `#transaction` method and can be brittle when upgrading. If you think that our version of `#transaction` is out of sync with the version of rails in our gemspec, please open a ticket and let us know. Our custom transaction method can be found in `activerecord/connection_adapters/sqlserver/core_ext/database_statements.rb`.
118+
119+
```ruby
120+
ActiveRecord::ConnectionAdapters::SQLServerAdapter.retry_deadlock_victim = true
121+
```
122+
123+
114124
#### Force Schema To Lowercase
115125

116126
Although it is not necessary, the Ruby convention is to use lowercase method names. If your database schema is in upper or mixed case, we can force all table and column names during the schema reflection process to be lowercase. Add this to your config/initializers file for the adapter.

activerecord-sqlserver-adapter.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ Gem::Specification.new do |s|
1717
s.require_path = 'lib'
1818
s.rubyforge_project = 'activerecord-sqlserver-adapter'
1919

20-
s.add_dependency('activerecord', '~> 3.1.0')
20+
s.add_dependency('activerecord', '~> 3.1.3')
2121
end
2222

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
module ActiveRecord
2+
module ConnectionAdapters
3+
module Sqlserver
4+
module CoreExt
5+
module DatabaseStatements
6+
7+
# This is a copy of the current (3.1.3) ActiveRecord's transaction method. We should propose
8+
# a patch to the default transaction method to make it more callback for adapters that want to
9+
# do deadlock retry logic. Because this is a copy, we really need to keep an eye out on this when
10+
# upgradding the adapter.
11+
def transaction_with_retry_deadlock_victim(options = {})
12+
options.assert_valid_keys :requires_new, :joinable
13+
14+
last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil
15+
if options.has_key?(:joinable)
16+
@transaction_joinable = options[:joinable]
17+
else
18+
@transaction_joinable = true
19+
end
20+
requires_new = options[:requires_new] || !last_transaction_joinable
21+
22+
transaction_open = false
23+
@_current_transaction_records ||= []
24+
25+
begin
26+
if block_given?
27+
if requires_new || open_transactions == 0
28+
if open_transactions == 0
29+
begin_db_transaction
30+
elsif requires_new
31+
create_savepoint
32+
end
33+
increment_open_transactions
34+
transaction_open = true
35+
@_current_transaction_records.push([])
36+
end
37+
yield
38+
end
39+
rescue Exception => database_transaction_rollback
40+
if transaction_open && !outside_transaction?
41+
transaction_open = false
42+
decrement_open_transactions
43+
# handle deadlock victim retries at the outermost transaction
44+
if open_transactions == 0
45+
if database_transaction_rollback.is_a?(::ActiveRecord::DeadlockVictim)
46+
# SQL Server has already rolled back, so rollback activerecord's history
47+
rollback_transaction_records(true)
48+
retry
49+
else
50+
rollback_db_transaction
51+
rollback_transaction_records(true)
52+
end
53+
else
54+
rollback_to_savepoint
55+
rollback_transaction_records(false)
56+
end
57+
end
58+
raise unless database_transaction_rollback.is_a?(::ActiveRecord::Rollback)
59+
end
60+
ensure
61+
@transaction_joinable = last_transaction_joinable
62+
63+
if outside_transaction?
64+
@open_transactions = 0
65+
elsif transaction_open
66+
decrement_open_transactions
67+
begin
68+
if open_transactions == 0
69+
commit_db_transaction
70+
commit_transaction_records
71+
else
72+
release_savepoint
73+
save_point_records = @_current_transaction_records.pop
74+
unless save_point_records.blank?
75+
@_current_transaction_records.push([]) if @_current_transaction_records.empty?
76+
@_current_transaction_records.last.concat(save_point_records)
77+
end
78+
end
79+
rescue Exception => database_transaction_rollback
80+
if open_transactions == 0
81+
rollback_db_transaction
82+
rollback_transaction_records(true)
83+
else
84+
rollback_to_savepoint
85+
rollback_transaction_records(false)
86+
end
87+
raise
88+
end
89+
end
90+
end
91+
92+
end
93+
end
94+
end
95+
end
96+
end
97+

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 6 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module ConnectionAdapters
33
module Sqlserver
44
module DatabaseStatements
55

6+
include CoreExt::DatabaseStatements
7+
68
def select_rows(sql, name = nil)
79
raw_select sql, name, [], :fetch => :rows
810
end
@@ -45,88 +47,11 @@ def supports_statement_cache?
4547
true
4648
end
4749

48-
def transaction(options = {}, &block)
49-
retry_deadlock_victim? ? transaction_with_retry_deadlock_victim(options, &block) : super(options, &block)
50-
end
51-
52-
def transaction_with_retry_deadlock_victim(options = {})
53-
options.assert_valid_keys :requires_new, :joinable
54-
55-
last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil
56-
if options.has_key?(:joinable)
57-
@transaction_joinable = options[:joinable]
50+
def transaction(options = {})
51+
if retry_deadlock_victim?
52+
block_given? ? transaction_with_retry_deadlock_victim(options) { yield } : transaction_with_retry_deadlock_victim(options)
5853
else
59-
@transaction_joinable = true
60-
end
61-
requires_new = options[:requires_new] || !last_transaction_joinable
62-
63-
transaction_open = false
64-
@_current_transaction_records ||= []
65-
66-
begin
67-
if block_given?
68-
if requires_new || open_transactions == 0
69-
if open_transactions == 0
70-
begin_db_transaction
71-
elsif requires_new
72-
create_savepoint
73-
end
74-
increment_open_transactions
75-
transaction_open = true
76-
@_current_transaction_records.push([])
77-
end
78-
yield
79-
end
80-
rescue Exception => database_transaction_rollback
81-
if transaction_open && !outside_transaction?
82-
transaction_open = false
83-
decrement_open_transactions
84-
# handle deadlock victim retries at the outermost transaction
85-
if open_transactions == 0
86-
if database_transaction_rollback.is_a?(DeadlockVictim)
87-
# SQL Server has already rolled back, so rollback activerecord's history
88-
rollback_transaction_records(true)
89-
retry
90-
else
91-
rollback_db_transaction
92-
rollback_transaction_records(true)
93-
end
94-
else
95-
rollback_to_savepoint
96-
rollback_transaction_records(false)
97-
end
98-
end
99-
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
100-
end
101-
ensure
102-
@transaction_joinable = last_transaction_joinable
103-
104-
if outside_transaction?
105-
@open_transactions = 0
106-
elsif transaction_open
107-
decrement_open_transactions
108-
begin
109-
if open_transactions == 0
110-
commit_db_transaction
111-
commit_transaction_records
112-
else
113-
release_savepoint
114-
save_point_records = @_current_transaction_records.pop
115-
unless save_point_records.blank?
116-
@_current_transaction_records.push([]) if @_current_transaction_records.empty?
117-
@_current_transaction_records.last.concat(save_point_records)
118-
end
119-
end
120-
rescue Exception => database_transaction_rollback
121-
if open_transactions == 0
122-
rollback_db_transaction
123-
rollback_transaction_records(true)
124-
else
125-
rollback_to_savepoint
126-
rollback_transaction_records(false)
127-
end
128-
raise
129-
end
54+
block_given? ? super(options) { yield } : super(options)
13055
end
13156
end
13257

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'active_record'
33
require 'active_record/connection_adapters/abstract_adapter'
44
require 'active_record/connection_adapters/sqlserver/core_ext/active_record'
5+
require 'active_record/connection_adapters/sqlserver/core_ext/database_statements'
56
require 'active_record/connection_adapters/sqlserver/database_limits'
67
require 'active_record/connection_adapters/sqlserver/database_statements'
78
require 'active_record/connection_adapters/sqlserver/errors'
@@ -485,7 +486,7 @@ def with_sqlserver_error_handling
485486
rescue Exception => e
486487
case translate_exception(e,e.message)
487488
when LostConnection; retry if auto_reconnected?
488-
when DeadlockVictim; retry if retry_deadlock_victim? && self.open_transactions == 0
489+
when DeadlockVictim; retry if retry_deadlock_victim? && open_transactions == 0
489490
end
490491
raise
491492
end

0 commit comments

Comments
 (0)