Skip to content

Commit 3ab1753

Browse files
committed
Implement #add_lock! in a way that does not monkey patch and works with both normal and association finder sql. Also implement test (even the PostgresSQL dueling ones) for the pessimistic locking.
1 parent 0e0125b commit 3ab1753

File tree

3 files changed

+104
-35
lines changed

3 files changed

+104
-35
lines changed

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,10 @@ def add_limit_offset!(sql, options)
351351

352352
def add_lock!(sql, options)
353353
# http://blog.sqlauthority.com/2007/04/27/sql-server-2005-locking-hints-and-examples/
354-
case lock = options[:lock]
355-
when true then sql << ' WITH(HOLDLOCK, ROWLOCK) '
356-
when String then sql << " #{lock} "
357-
end
354+
return unless options[:lock]
355+
lock_type = options[:lock] == true ? 'WITH(HOLDLOCK, ROWLOCK)' : options[:lock]
356+
from_table = sql.match(/FROM(.*)WHERE/im)[1]
357+
sql.sub! from_table, "#{from_table}#{lock_type} "
358358
end
359359

360360
def empty_insert_statement(table_name)

lib/core_ext/active_record.rb

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,6 @@ def reset_column_information_with_sqlserver_columns_cache_support
1818

1919
private
2020

21-
# Add basic support for SQL server locking hints
22-
# In the case of SQL server, the lock value must follow the FROM clause
23-
# Mysql: SELECT * FROM tst where testID = 10 LOCK IN share mode
24-
# SQLServer: SELECT * from tst WITH (HOLDLOCK, ROWLOCK) where testID = 10
25-
# h-lame: OK, so these 2 methods should be a patch to rails ideally, so we don't
26-
# have to play catch up against rails itself should construct_finder_sql ever
27-
# change
28-
def construct_finder_sql(options)
29-
scope = scope(:find)
30-
sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} "
31-
sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
32-
33-
add_lock!(sql, options, scope) if ActiveRecord::Base.connection.adapter_name == "SQLServer" && !options[:lock].blank? # SQLServer
34-
35-
# merge_joins isn't defined in 2.1.1, but appears in edge
36-
if defined?(merge_joins)
37-
# The next line may fail with a nil error under 2.1.1 or other non-edge rails versions - Use this instead: add_joins!(sql, options, scope)
38-
add_joins!(sql, options[:joins], scope)
39-
else
40-
add_joins!(sql, options, scope)
41-
end
42-
43-
add_conditions!(sql, options[:conditions], scope)
44-
45-
add_group!(sql, options[:group], scope)
46-
add_order!(sql, options[:order], scope)
47-
add_limit!(sql, options, scope)
48-
add_lock!(sql, options, scope) unless ActiveRecord::Base.connection.adapter_name == "SQLServer" # Not SQLServer
49-
sql
50-
end
51-
5221
# Overwrite the ActiveRecord::Base method for SQL server.
5322
# GROUP BY is necessary for distinct orderings
5423
def construct_finder_sql_for_association_limiting(options, join_dependency)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
require 'cases/sqlserver_helper'
2+
require 'models/person'
3+
require 'models/reader'
4+
5+
class PessimisticLockingTestSqlserver < ActiveRecord::TestCase
6+
7+
self.use_transactional_fixtures = false
8+
fixtures :people, :readers
9+
10+
def setup
11+
Person.columns; Reader.columns # Avoid introspection queries during tests.
12+
end
13+
14+
context 'For simple finds with default lock option' do
15+
16+
should 'lock with simple find' do
17+
assert_nothing_raised do
18+
Person.transaction do
19+
Person.find 1, :lock => true
20+
end
21+
end
22+
end
23+
24+
should 'lock with scoped find' do
25+
assert_nothing_raised do
26+
Person.transaction do
27+
Person.with_scope(:find => { :lock => true }) do
28+
Person.find 1
29+
end
30+
end
31+
end
32+
end
33+
34+
should 'lock with eager find' do
35+
assert_nothing_raised do
36+
Person.transaction do
37+
Person.find 1, :include => :readers, :lock => true
38+
end
39+
end
40+
end
41+
42+
should 'reload with lock when #lock! called' do
43+
assert_nothing_raised do
44+
Person.transaction do
45+
person = Person.find 1
46+
old, person.first_name = person.first_name, 'fooman'
47+
person.lock!
48+
assert_equal old, person.first_name
49+
end
50+
end
51+
end
52+
53+
end
54+
55+
context 'For dueling concurrent connections' do
56+
57+
use_concurrent_connections
58+
59+
should 'no locks does not wait' do
60+
first, second = duel { Person.find 1 }
61+
assert first.end > second.end
62+
end
63+
64+
should 'that second lock waits' do
65+
assert [0.2, 1, 5].any? { |zzz|
66+
first, second = duel(zzz) { Person.find 1, :lock => true }
67+
second.end > first.end
68+
}
69+
end
70+
71+
end
72+
73+
74+
protected
75+
76+
def duel(zzz = 5)
77+
t0, t1, t2, t3 = nil, nil, nil, nil
78+
a = Thread.new do
79+
t0 = Time.now
80+
Person.transaction do
81+
yield
82+
sleep zzz # block thread 2 for zzz seconds
83+
end
84+
t1 = Time.now
85+
end
86+
b = Thread.new do
87+
sleep zzz / 2.0 # ensure thread 1 tx starts first
88+
t2 = Time.now
89+
Person.transaction { yield }
90+
t3 = Time.now
91+
end
92+
a.join
93+
b.join
94+
assert t1 > t0 + zzz
95+
assert t2 > t0
96+
assert t3 > t2
97+
[t0.to_f..t1.to_f, t2.to_f..t3.to_f]
98+
end
99+
100+
end

0 commit comments

Comments
 (0)