with_advisory_lock 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +10 -10
- data/Appraisals +4 -13
- data/Gemfile +2 -2
- data/README.md +30 -3
- data/gemfiles/{activerecord_3.2.gemfile → activerecord_4.2.gemfile} +1 -1
- data/gemfiles/{activerecord_4.0.gemfile → activerecord_5.0.gemfile} +1 -1
- data/lib/with_advisory_lock/base.rb +16 -5
- data/lib/with_advisory_lock/concern.rb +6 -5
- data/lib/with_advisory_lock/flock.rb +4 -1
- data/lib/with_advisory_lock/mysql.rb +7 -1
- data/lib/with_advisory_lock/postgresql.rb +13 -2
- data/lib/with_advisory_lock/version.rb +1 -1
- data/test/nesting_test.rb +9 -9
- data/test/options_test.rb +64 -0
- data/test/parallelism_test.rb +1 -2
- data/test/shared_test.rb +131 -0
- data/test/transaction_test.rb +70 -0
- data/tests.sh +1 -1
- metadata +51 -48
- data/gemfiles/activerecord_4.1.gemfile +0 -19
- data/gemfiles/activerecord_edge.gemfile +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d83c8384aa5b5b063851f1bcef61584c552c6845
|
4
|
+
data.tar.gz: 2ff00f6afba93642d8d2bcb0c6ae639dec2ff913
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b1529ea567b8f99008590582827e0b7b4a5c03af8fa779253f68c40aac1c9eaaa0cea4537f1007027c3148422af3ca59579416443443328aa27775560deda22
|
7
|
+
data.tar.gz: 06f6b2a54dc18806663041c0954a5d222ed533023e8e01a5c61a21e078836cc9e4fd00a822b35f825da1e144cdedeaf8ee396e2224ff83db413ae792288ad83a
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
language: ruby
|
2
2
|
|
3
3
|
rvm:
|
4
|
-
-
|
5
|
-
- 2.
|
6
|
-
-
|
7
|
-
# TODO - rbx-2
|
4
|
+
- 2.4.0
|
5
|
+
- 2.3.3
|
6
|
+
- 2.2.6
|
8
7
|
|
9
8
|
gemfile:
|
10
|
-
- gemfiles/
|
11
|
-
- gemfiles/activerecord_4.
|
12
|
-
- gemfiles/activerecord_4.1.gemfile
|
13
|
-
- gemfiles/activerecord_edge.gemfile
|
9
|
+
- gemfiles/activerecord_5.0.gemfile
|
10
|
+
- gemfiles/activerecord_4.2.gemfile
|
14
11
|
|
15
12
|
env:
|
16
|
-
- DB=sqlite
|
17
|
-
- DB=mysql
|
18
13
|
- DB=postgresql
|
14
|
+
- DB=mysql
|
15
|
+
- DB=sqlite
|
16
|
+
|
17
|
+
before_install:
|
18
|
+
- gem install bundler
|
19
19
|
|
20
20
|
script: WITH_ADVISORY_LOCK_PREFIX=$TRAVIS_JOB_ID bundle exec rake --trace
|
21
21
|
|
data/Appraisals
CHANGED
@@ -1,16 +1,7 @@
|
|
1
|
-
appraise "activerecord-
|
2
|
-
gem
|
1
|
+
appraise "activerecord-4.2" do
|
2
|
+
gem "activerecord", "~> 4.2.0"
|
3
3
|
end
|
4
4
|
|
5
|
-
appraise "activerecord-
|
6
|
-
gem "activerecord", "~>
|
7
|
-
end
|
8
|
-
|
9
|
-
appraise "activerecord-4.1" do
|
10
|
-
gem "activerecord", "~> 4.1.0"
|
11
|
-
end
|
12
|
-
|
13
|
-
appraise "activerecord-edge" do
|
14
|
-
gem "activerecord", github: "rails/rails"
|
15
|
-
gem 'arel', github: 'rails/arel'
|
5
|
+
appraise "activerecord-5.0" do
|
6
|
+
gem "activerecord", "~> 5.0.0"
|
16
7
|
end
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# with_advisory_lock
|
2
2
|
|
3
|
-
Adds advisory locking (mutexes) to ActiveRecord
|
3
|
+
Adds advisory locking (mutexes) to ActiveRecord 4.2 and 5.0, with ruby 2.4, 2.3 or 2.2, when used with
|
4
4
|
[MySQL](http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock)
|
5
5
|
or [PostgreSQL](http://www.postgresql.org/docs/9.3/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS).
|
6
6
|
SQLite resorts to file locking.
|
@@ -34,8 +34,8 @@ end
|
|
34
34
|
|
35
35
|
### Lock wait timeouts
|
36
36
|
|
37
|
-
|
38
|
-
which means wait indefinitely for the lock.
|
37
|
+
```with_advisory_lock``` takes an options hash as the second parameter.
|
38
|
+
The ```timeout_seconds``` option defaults to ```nil```, which means wait indefinitely for the lock.
|
39
39
|
|
40
40
|
A value of zero will try the lock only once. If the lock is acquired, the block
|
41
41
|
will be yielded to. If the lock is currently being held, the block will not be called.
|
@@ -43,6 +43,23 @@ will be yielded to. If the lock is currently being held, the block will not be c
|
|
43
43
|
Note that if a non-nil value is provided for `timeout_seconds`, the block will not be invoked if
|
44
44
|
the lock cannot be acquired within that time-frame.
|
45
45
|
|
46
|
+
For backwards compatability, the timeout value can be specified directly as the second parameter.
|
47
|
+
|
48
|
+
### Shared locks
|
49
|
+
|
50
|
+
The ```shared``` option defaults to ```false``` which means an exclusive lock will be obtained.
|
51
|
+
Setting ```shared``` to ```true``` will allow locks to be obtained by multiple actors
|
52
|
+
as long as they are all shared locks.
|
53
|
+
|
54
|
+
Note: MySQL does not support shared locks.
|
55
|
+
|
56
|
+
### Transaction-level locks
|
57
|
+
|
58
|
+
PostgreSQL supports transaction-level locks which remain held until the transaction completes.
|
59
|
+
You can enable this by setting the ```transaction``` option to ```true```.
|
60
|
+
|
61
|
+
Note: transaction-level locks will not be reflected by `.current_advisory_lock` when the block has returned.
|
62
|
+
|
46
63
|
### Return values
|
47
64
|
|
48
65
|
The return value of `with_advisory_lock_result` is a `WithAdvisoryLock::Result` instance,
|
@@ -117,6 +134,10 @@ you will be releasing the parent lock (!!!). A ```NestedAdvisoryLockError```will
|
|
117
134
|
in this case. If you ask for the same lock name, ```with_advisory_lock``` won't ask for the
|
118
135
|
lock again, and the block given will be yielded to.
|
119
136
|
|
137
|
+
### Is clustered MySQL supported?
|
138
|
+
|
139
|
+
[No.](https://github.com/mceachen/with_advisory_lock/issues/16)
|
140
|
+
|
120
141
|
### There are many ```lock-*``` files in my project directory after test runs
|
121
142
|
|
122
143
|
This is expected if you aren't using MySQL or Postgresql for your tests.
|
@@ -139,6 +160,12 @@ end
|
|
139
160
|
|
140
161
|
## Changelog
|
141
162
|
|
163
|
+
### 3.1.0
|
164
|
+
|
165
|
+
* [Jason Weathered](https://github.com/jasoncodes) Added new shared and transaction-level lock options ([Pull request 21](https://github.com/mceachen/with_advisory_lock/pull/21)). Thanks!
|
166
|
+
* Added ActiveRecord 5.0 to build matrix. Dropped 3.2, 4.0, and 4.1 (which no longer get security updates: http://rubyonrails.org/security/)
|
167
|
+
* Replaced ruby 1.9 and 2.0 (both EOL) with ruby 2.2 and 2.3 (see https://www.ruby-lang.org/en/downloads/)
|
168
|
+
|
142
169
|
### 3.0.0
|
143
170
|
|
144
171
|
* Added jruby/PostgreSQL support for Rails 4.x
|
@@ -16,19 +16,30 @@ module WithAdvisoryLock
|
|
16
16
|
|
17
17
|
FAILED_TO_LOCK = Result.new(false)
|
18
18
|
|
19
|
+
LockStackItem = Struct.new(:name, :shared)
|
20
|
+
|
19
21
|
class Base
|
20
|
-
attr_reader :connection, :lock_name, :timeout_seconds
|
22
|
+
attr_reader :connection, :lock_name, :timeout_seconds, :shared, :transaction
|
23
|
+
|
24
|
+
def initialize(connection, lock_name, options)
|
25
|
+
options = {timeout_seconds: options} unless options.respond_to?(:fetch)
|
26
|
+
options.assert_valid_keys :timeout_seconds, :shared, :transaction
|
21
27
|
|
22
|
-
def initialize(connection, lock_name, timeout_seconds)
|
23
28
|
@connection = connection
|
24
29
|
@lock_name = lock_name
|
25
|
-
@timeout_seconds = timeout_seconds
|
30
|
+
@timeout_seconds = options.fetch(:timeout_seconds, nil)
|
31
|
+
@shared = options.fetch(:shared, false)
|
32
|
+
@transaction = options.fetch(:transaction, false)
|
26
33
|
end
|
27
34
|
|
28
35
|
def lock_str
|
29
36
|
@lock_str ||= "#{ENV['WITH_ADVISORY_LOCK_PREFIX'].to_s}#{lock_name.to_s}"
|
30
37
|
end
|
31
38
|
|
39
|
+
def lock_stack_item
|
40
|
+
@lock_stack_item ||= LockStackItem.new(lock_str, shared)
|
41
|
+
end
|
42
|
+
|
32
43
|
def self.lock_stack
|
33
44
|
# access doesn't need to be synchronized as it is only accessed by the current thread.
|
34
45
|
Thread.current[:with_advisory_lock_stack] ||= []
|
@@ -36,7 +47,7 @@ module WithAdvisoryLock
|
|
36
47
|
delegate :lock_stack, to: 'self.class'
|
37
48
|
|
38
49
|
def already_locked?
|
39
|
-
lock_stack.include?
|
50
|
+
lock_stack.include? lock_stack_item
|
40
51
|
end
|
41
52
|
|
42
53
|
def with_advisory_lock_if_needed(&block)
|
@@ -73,7 +84,7 @@ module WithAdvisoryLock
|
|
73
84
|
def yield_with_lock
|
74
85
|
if try_lock
|
75
86
|
begin
|
76
|
-
lock_stack.push(
|
87
|
+
lock_stack.push(lock_stack_item)
|
77
88
|
result = block_given? ? yield : nil
|
78
89
|
Result.new(true, result)
|
79
90
|
ensure
|
@@ -6,13 +6,13 @@ module WithAdvisoryLock
|
|
6
6
|
delegate :with_advisory_lock, :advisory_lock_exists?, to: 'self.class'
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
-
def with_advisory_lock(lock_name,
|
10
|
-
result = with_advisory_lock_result(lock_name,
|
9
|
+
def with_advisory_lock(lock_name, options={}, &block)
|
10
|
+
result = with_advisory_lock_result(lock_name, options, &block)
|
11
11
|
result.lock_was_acquired? ? result.result : false
|
12
12
|
end
|
13
13
|
|
14
|
-
def with_advisory_lock_result(lock_name,
|
15
|
-
impl = impl_class.new(connection, lock_name,
|
14
|
+
def with_advisory_lock_result(lock_name, options={}, &block)
|
15
|
+
impl = impl_class.new(connection, lock_name, options)
|
16
16
|
impl.with_advisory_lock_if_needed(&block)
|
17
17
|
end
|
18
18
|
|
@@ -22,7 +22,8 @@ module WithAdvisoryLock
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def current_advisory_lock
|
25
|
-
WithAdvisoryLock::Base.lock_stack.first
|
25
|
+
lock_stack_key = WithAdvisoryLock::Base.lock_stack.first
|
26
|
+
lock_stack_key && lock_stack_key[0]
|
26
27
|
end
|
27
28
|
|
28
29
|
private
|
@@ -20,7 +20,10 @@ module WithAdvisoryLock
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def try_lock
|
23
|
-
|
23
|
+
if transaction
|
24
|
+
raise ArgumentError, 'transaction level locks are not supported on SQLite'
|
25
|
+
end
|
26
|
+
0 == file_io.flock((shared ? File::LOCK_SH : File::LOCK_EX) | File::LOCK_NB)
|
24
27
|
end
|
25
28
|
|
26
29
|
def release_lock
|
@@ -7,6 +7,12 @@ module WithAdvisoryLock
|
|
7
7
|
"MySQL doesn't support nested Advisory Locks",
|
8
8
|
lock_stack.dup)
|
9
9
|
end
|
10
|
+
if shared
|
11
|
+
raise ArgumentError, 'shared locks are not supported on MySQL'
|
12
|
+
end
|
13
|
+
if transaction
|
14
|
+
raise ArgumentError, 'transaction level locks are not supported on MySQL'
|
15
|
+
end
|
10
16
|
execute_successful?("GET_LOCK(#{quoted_lock_str}, 0)")
|
11
17
|
end
|
12
18
|
|
@@ -21,7 +27,7 @@ module WithAdvisoryLock
|
|
21
27
|
|
22
28
|
# MySQL doesn't support nested locks:
|
23
29
|
def already_locked?
|
24
|
-
lock_stack.last ==
|
30
|
+
lock_stack.last == lock_stack_item
|
25
31
|
end
|
26
32
|
|
27
33
|
# MySQL wants a string as the lock key.
|
@@ -2,11 +2,22 @@ module WithAdvisoryLock
|
|
2
2
|
class PostgreSQL < Base
|
3
3
|
# See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
|
4
4
|
def try_lock
|
5
|
-
|
5
|
+
pg_function = "pg_try_advisory#{transaction ? '_xact' : ''}_lock#{shared ? '_shared' : ''}"
|
6
|
+
execute_successful?(pg_function)
|
6
7
|
end
|
7
8
|
|
8
9
|
def release_lock
|
9
|
-
|
10
|
+
return if transaction
|
11
|
+
pg_function = "pg_advisory_unlock#{shared ? '_shared' : ''}"
|
12
|
+
execute_successful?(pg_function)
|
13
|
+
rescue ActiveRecord::StatementInvalid => e
|
14
|
+
raise unless e.message =~ / ERROR: +current transaction is aborted,/
|
15
|
+
begin
|
16
|
+
connection.rollback_db_transaction
|
17
|
+
execute_successful?(pg_function)
|
18
|
+
ensure
|
19
|
+
connection.begin_db_transaction
|
20
|
+
end
|
10
21
|
end
|
11
22
|
|
12
23
|
def execute_successful?(pg_function)
|
data/test/nesting_test.rb
CHANGED
@@ -15,12 +15,12 @@ describe "lock nesting" do
|
|
15
15
|
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
16
16
|
impl.lock_stack.must_be_empty
|
17
17
|
Tag.with_advisory_lock("first") do
|
18
|
-
impl.lock_stack.must_equal %w(first)
|
18
|
+
impl.lock_stack.map(&:name).must_equal %w(first)
|
19
19
|
# Even MySQL should be OK with this:
|
20
20
|
Tag.with_advisory_lock("first") do
|
21
|
-
impl.lock_stack.must_equal %w(first)
|
21
|
+
impl.lock_stack.map(&:name).must_equal %w(first)
|
22
22
|
end
|
23
|
-
impl.lock_stack.must_equal %w(first)
|
23
|
+
impl.lock_stack.map(&:name).must_equal %w(first)
|
24
24
|
end
|
25
25
|
impl.lock_stack.must_be_empty
|
26
26
|
end
|
@@ -33,7 +33,7 @@ describe "lock nesting" do
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
36
|
-
exc.lock_stack.must_equal %w(first)
|
36
|
+
exc.lock_stack.map(&:name).must_equal %w(first)
|
37
37
|
end
|
38
38
|
|
39
39
|
it "supports nested advisory locks with !MySQL" do
|
@@ -41,19 +41,19 @@ describe "lock nesting" do
|
|
41
41
|
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
42
42
|
impl.lock_stack.must_be_empty
|
43
43
|
Tag.with_advisory_lock("first") do
|
44
|
-
impl.lock_stack.must_equal %w(first)
|
44
|
+
impl.lock_stack.map(&:name).must_equal %w(first)
|
45
45
|
Tag.with_advisory_lock("second") do
|
46
|
-
impl.lock_stack.must_equal %w(first second)
|
46
|
+
impl.lock_stack.map(&:name).must_equal %w(first second)
|
47
47
|
Tag.with_advisory_lock("first") do
|
48
48
|
# Shouldn't ask for another lock:
|
49
|
-
impl.lock_stack.must_equal %w(first second)
|
49
|
+
impl.lock_stack.map(&:name).must_equal %w(first second)
|
50
50
|
Tag.with_advisory_lock("second") do
|
51
51
|
# Shouldn't ask for another lock:
|
52
|
-
impl.lock_stack.must_equal %w(first second)
|
52
|
+
impl.lock_stack.map(&:name).must_equal %w(first second)
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
56
|
-
impl.lock_stack.must_equal %w(first)
|
56
|
+
impl.lock_stack.map(&:name).must_equal %w(first)
|
57
57
|
end
|
58
58
|
impl.lock_stack.must_be_empty
|
59
59
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe 'options parsing' do
|
4
|
+
def parse_options(options)
|
5
|
+
WithAdvisoryLock::Base.new(mock, mock, options)
|
6
|
+
end
|
7
|
+
|
8
|
+
specify 'defaults (empty hash)' do
|
9
|
+
impl = parse_options({})
|
10
|
+
impl.timeout_seconds.must_equal nil
|
11
|
+
impl.shared.must_equal false
|
12
|
+
impl.transaction.must_equal false
|
13
|
+
end
|
14
|
+
|
15
|
+
specify 'nil sets timeout to nil' do
|
16
|
+
impl = parse_options(nil)
|
17
|
+
impl.timeout_seconds.must_equal nil
|
18
|
+
impl.shared.must_equal false
|
19
|
+
impl.transaction.must_equal false
|
20
|
+
end
|
21
|
+
|
22
|
+
specify 'integer sets timeout to value' do
|
23
|
+
impl = parse_options(42)
|
24
|
+
impl.timeout_seconds.must_equal 42
|
25
|
+
impl.shared.must_equal false
|
26
|
+
impl.transaction.must_equal false
|
27
|
+
end
|
28
|
+
|
29
|
+
specify 'hash with invalid key errors' do
|
30
|
+
proc {
|
31
|
+
parse_options(foo: 42)
|
32
|
+
}.must_raise ArgumentError
|
33
|
+
end
|
34
|
+
|
35
|
+
specify 'hash with timeout_seconds sets timeout to value' do
|
36
|
+
impl = parse_options(timeout_seconds: 123)
|
37
|
+
impl.timeout_seconds.must_equal 123
|
38
|
+
impl.shared.must_equal false
|
39
|
+
impl.transaction.must_equal false
|
40
|
+
end
|
41
|
+
|
42
|
+
specify 'hash with shared option sets shared to true' do
|
43
|
+
impl = parse_options(shared: true)
|
44
|
+
impl.timeout_seconds.must_equal nil
|
45
|
+
impl.shared.must_equal true
|
46
|
+
impl.transaction.must_equal false
|
47
|
+
end
|
48
|
+
|
49
|
+
specify 'hash with transaction option set transaction to true' do
|
50
|
+
impl = parse_options(transaction: true)
|
51
|
+
impl.timeout_seconds.must_equal nil
|
52
|
+
impl.shared.must_equal false
|
53
|
+
impl.transaction.must_equal true
|
54
|
+
end
|
55
|
+
|
56
|
+
specify 'hash with multiple keys sets options' do
|
57
|
+
foo = mock
|
58
|
+
bar = mock
|
59
|
+
impl = parse_options(timeout_seconds: foo, shared: bar)
|
60
|
+
impl.timeout_seconds.must_equal foo
|
61
|
+
impl.shared.must_equal bar
|
62
|
+
impl.transaction.must_equal false
|
63
|
+
end
|
64
|
+
end
|
data/test/parallelism_test.rb
CHANGED
@@ -4,7 +4,7 @@ require 'forwardable'
|
|
4
4
|
describe 'parallelism' do
|
5
5
|
class FindOrCreateWorker
|
6
6
|
extend Forwardable
|
7
|
-
def_delegators :@thread, :join, :wakeup, :
|
7
|
+
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
8
8
|
|
9
9
|
def initialize(name, use_advisory_lock)
|
10
10
|
@name = name
|
@@ -72,4 +72,3 @@ describe 'parallelism' do
|
|
72
72
|
Label.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
73
73
|
end
|
74
74
|
end
|
75
|
-
|
data/test/shared_test.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe 'shared locks' do
|
4
|
+
def supported?
|
5
|
+
env_db != :mysql
|
6
|
+
end
|
7
|
+
|
8
|
+
class SharedTestWorker
|
9
|
+
def initialize(shared)
|
10
|
+
@shared = shared
|
11
|
+
|
12
|
+
@locked = nil
|
13
|
+
@cleanup = false
|
14
|
+
@thread = Thread.new { work }
|
15
|
+
end
|
16
|
+
|
17
|
+
def locked?
|
18
|
+
sleep 0.01 while @locked.nil? && @thread.alive?
|
19
|
+
@locked
|
20
|
+
end
|
21
|
+
|
22
|
+
def cleanup!
|
23
|
+
@cleanup = true
|
24
|
+
@thread.join
|
25
|
+
raise if @thread.status.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def work
|
31
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
32
|
+
Tag.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do
|
33
|
+
@locked = true
|
34
|
+
sleep 0.01 until @cleanup
|
35
|
+
end
|
36
|
+
@locked = false
|
37
|
+
sleep 0.01 until @cleanup
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'does not allow two exclusive locks' do
|
43
|
+
one = SharedTestWorker.new(false)
|
44
|
+
one.locked?.must_equal true
|
45
|
+
|
46
|
+
two = SharedTestWorker.new(false)
|
47
|
+
two.locked?.must_equal false
|
48
|
+
|
49
|
+
one.cleanup!
|
50
|
+
two.cleanup!
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'not supported' do
|
54
|
+
before do
|
55
|
+
skip if supported?
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'raises an error when attempting to use a shared lock' do
|
59
|
+
one = SharedTestWorker.new(true)
|
60
|
+
one.locked?.must_equal nil
|
61
|
+
exception = proc {
|
62
|
+
one.cleanup!
|
63
|
+
}.must_raise ArgumentError
|
64
|
+
exception.message.must_include 'not supported'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'supported' do
|
69
|
+
before do
|
70
|
+
skip unless supported?
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'does allow two shared locks' do
|
74
|
+
one = SharedTestWorker.new(true)
|
75
|
+
one.locked?.must_equal true
|
76
|
+
|
77
|
+
two = SharedTestWorker.new(true)
|
78
|
+
two.locked?.must_equal true
|
79
|
+
|
80
|
+
one.cleanup!
|
81
|
+
two.cleanup!
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'does not allow exclusive lock with shared lock' do
|
85
|
+
one = SharedTestWorker.new(true)
|
86
|
+
one.locked?.must_equal true
|
87
|
+
|
88
|
+
two = SharedTestWorker.new(false)
|
89
|
+
two.locked?.must_equal false
|
90
|
+
|
91
|
+
three = SharedTestWorker.new(true)
|
92
|
+
three.locked?.must_equal true
|
93
|
+
|
94
|
+
one.cleanup!
|
95
|
+
two.cleanup!
|
96
|
+
three.cleanup!
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'does not allow shared lock with exclusive lock' do
|
100
|
+
one = SharedTestWorker.new(false)
|
101
|
+
one.locked?.must_equal true
|
102
|
+
|
103
|
+
two = SharedTestWorker.new(true)
|
104
|
+
two.locked?.must_equal false
|
105
|
+
|
106
|
+
one.cleanup!
|
107
|
+
two.cleanup!
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'PostgreSQL' do
|
111
|
+
before do
|
112
|
+
skip unless env_db == :postgresql
|
113
|
+
end
|
114
|
+
|
115
|
+
def pg_lock_modes
|
116
|
+
ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'allows shared lock to be upgraded to an exclusive lock' do
|
120
|
+
pg_lock_modes.must_equal %w[]
|
121
|
+
Tag.with_advisory_lock 'test', shared: true do
|
122
|
+
pg_lock_modes.must_equal %w[ShareLock]
|
123
|
+
Tag.with_advisory_lock 'test', shared: false do
|
124
|
+
pg_lock_modes.must_equal %w[ShareLock ExclusiveLock]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
pg_lock_modes.must_equal %w[]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe 'transaction scoping' do
|
4
|
+
def supported?
|
5
|
+
env_db == :postgresql
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'not supported' do
|
9
|
+
before do
|
10
|
+
skip if supported?
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'raises an error when attempting to use transaction level locks' do
|
14
|
+
Tag.transaction do
|
15
|
+
exception = proc {
|
16
|
+
Tag.with_advisory_lock 'test', transaction: true do
|
17
|
+
raise 'should not get here'
|
18
|
+
end
|
19
|
+
}.must_raise ArgumentError
|
20
|
+
exception.message.must_include 'not supported'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'supported' do
|
26
|
+
before do
|
27
|
+
skip unless env_db == :postgresql
|
28
|
+
end
|
29
|
+
|
30
|
+
def pg_lock_count
|
31
|
+
ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';").to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
specify 'session locks release after the block executes' do
|
35
|
+
Tag.transaction do
|
36
|
+
pg_lock_count.must_equal 0
|
37
|
+
Tag.with_advisory_lock 'test' do
|
38
|
+
pg_lock_count.must_equal 1
|
39
|
+
end
|
40
|
+
pg_lock_count.must_equal 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
specify 'session locks release when transaction fails inside block' do
|
45
|
+
Tag.transaction do
|
46
|
+
pg_lock_count.must_equal 0
|
47
|
+
|
48
|
+
exception = proc {
|
49
|
+
Tag.with_advisory_lock 'test' do
|
50
|
+
Tag.connection.execute 'SELECT 1/0;'
|
51
|
+
end
|
52
|
+
}.must_raise ActiveRecord::StatementInvalid
|
53
|
+
exception.message.must_include 'division by zero'
|
54
|
+
|
55
|
+
pg_lock_count.must_equal 0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
specify 'transaction level locks hold until the transaction completes' do
|
60
|
+
Tag.transaction do
|
61
|
+
pg_lock_count.must_equal 0
|
62
|
+
Tag.with_advisory_lock 'test', transaction: true do
|
63
|
+
pg_lock_count.must_equal 1
|
64
|
+
end
|
65
|
+
pg_lock_count.must_equal 1
|
66
|
+
end
|
67
|
+
pg_lock_count.must_equal 0
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/tests.sh
CHANGED
metadata
CHANGED
@@ -1,125 +1,125 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: with_advisory_lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew McEachen
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-02-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
14
15
|
requirement: !ruby/object:Gem::Requirement
|
15
16
|
requirements:
|
16
|
-
- -
|
17
|
+
- - ">="
|
17
18
|
- !ruby/object:Gem::Version
|
18
19
|
version: '3.2'
|
19
|
-
name: activerecord
|
20
|
-
prerelease: false
|
21
20
|
type: :runtime
|
21
|
+
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
+
name: thread_safe
|
28
29
|
requirement: !ruby/object:Gem::Requirement
|
29
30
|
requirements:
|
30
|
-
- -
|
31
|
+
- - ">="
|
31
32
|
- !ruby/object:Gem::Version
|
32
33
|
version: '0'
|
33
|
-
name: thread_safe
|
34
|
-
prerelease: false
|
35
34
|
type: :runtime
|
35
|
+
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
+
name: yard
|
42
43
|
requirement: !ruby/object:Gem::Requirement
|
43
44
|
requirements:
|
44
|
-
- -
|
45
|
+
- - ">="
|
45
46
|
- !ruby/object:Gem::Version
|
46
47
|
version: '0'
|
47
|
-
name: yard
|
48
|
-
prerelease: false
|
49
48
|
type: :development
|
49
|
+
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
56
57
|
requirement: !ruby/object:Gem::Requirement
|
57
58
|
requirements:
|
58
|
-
- -
|
59
|
+
- - ">="
|
59
60
|
- !ruby/object:Gem::Version
|
60
61
|
version: '0'
|
61
|
-
name: minitest
|
62
|
-
prerelease: false
|
63
62
|
type: :development
|
63
|
+
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-great_expectations
|
70
71
|
requirement: !ruby/object:Gem::Requirement
|
71
72
|
requirements:
|
72
|
-
- -
|
73
|
+
- - ">="
|
73
74
|
- !ruby/object:Gem::Version
|
74
75
|
version: '0'
|
75
|
-
name: minitest-great_expectations
|
76
|
-
prerelease: false
|
77
76
|
type: :development
|
77
|
+
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest-reporters
|
84
85
|
requirement: !ruby/object:Gem::Requirement
|
85
86
|
requirements:
|
86
|
-
- -
|
87
|
+
- - ">="
|
87
88
|
- !ruby/object:Gem::Version
|
88
89
|
version: '0'
|
89
|
-
name: minitest-reporters
|
90
|
-
prerelease: false
|
91
90
|
type: :development
|
91
|
+
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
+
name: mocha
|
98
99
|
requirement: !ruby/object:Gem::Requirement
|
99
100
|
requirements:
|
100
|
-
- -
|
101
|
+
- - ">="
|
101
102
|
- !ruby/object:Gem::Version
|
102
103
|
version: '0'
|
103
|
-
name: mocha
|
104
|
-
prerelease: false
|
105
104
|
type: :development
|
105
|
+
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
+
name: appraisal
|
112
113
|
requirement: !ruby/object:Gem::Requirement
|
113
114
|
requirements:
|
114
|
-
- -
|
115
|
+
- - ">="
|
115
116
|
- !ruby/object:Gem::Version
|
116
117
|
version: '0'
|
117
|
-
name: appraisal
|
118
|
-
prerelease: false
|
119
118
|
type: :development
|
119
|
+
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- -
|
122
|
+
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
description: Advisory locking for ActiveRecord
|
@@ -129,17 +129,15 @@ executables: []
|
|
129
129
|
extensions: []
|
130
130
|
extra_rdoc_files: []
|
131
131
|
files:
|
132
|
-
- .gitignore
|
133
|
-
- .travis.yml
|
132
|
+
- ".gitignore"
|
133
|
+
- ".travis.yml"
|
134
134
|
- Appraisals
|
135
135
|
- Gemfile
|
136
136
|
- LICENSE.txt
|
137
137
|
- README.md
|
138
138
|
- Rakefile
|
139
|
-
- gemfiles/
|
140
|
-
- gemfiles/
|
141
|
-
- gemfiles/activerecord_4.1.gemfile
|
142
|
-
- gemfiles/activerecord_edge.gemfile
|
139
|
+
- gemfiles/activerecord_4.2.gemfile
|
140
|
+
- gemfiles/activerecord_5.0.gemfile
|
143
141
|
- lib/with_advisory_lock.rb
|
144
142
|
- lib/with_advisory_lock/base.rb
|
145
143
|
- lib/with_advisory_lock/concern.rb
|
@@ -154,33 +152,36 @@ files:
|
|
154
152
|
- test/lock_test.rb
|
155
153
|
- test/minitest_helper.rb
|
156
154
|
- test/nesting_test.rb
|
155
|
+
- test/options_test.rb
|
157
156
|
- test/parallelism_test.rb
|
157
|
+
- test/shared_test.rb
|
158
158
|
- test/test_models.rb
|
159
159
|
- test/thread_test.rb
|
160
|
+
- test/transaction_test.rb
|
160
161
|
- tests.sh
|
161
162
|
- with_advisory_lock.gemspec
|
162
163
|
homepage: https://github.com/mceachen/with_advisory_lock
|
163
164
|
licenses:
|
164
165
|
- MIT
|
165
166
|
metadata: {}
|
166
|
-
post_install_message:
|
167
|
+
post_install_message:
|
167
168
|
rdoc_options: []
|
168
169
|
require_paths:
|
169
170
|
- lib
|
170
171
|
required_ruby_version: !ruby/object:Gem::Requirement
|
171
172
|
requirements:
|
172
|
-
- -
|
173
|
+
- - ">="
|
173
174
|
- !ruby/object:Gem::Version
|
174
175
|
version: '0'
|
175
176
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
177
|
requirements:
|
177
|
-
- -
|
178
|
+
- - ">="
|
178
179
|
- !ruby/object:Gem::Version
|
179
180
|
version: '0'
|
180
181
|
requirements: []
|
181
|
-
rubyforge_project:
|
182
|
-
rubygems_version: 2.1
|
183
|
-
signing_key:
|
182
|
+
rubyforge_project:
|
183
|
+
rubygems_version: 2.5.1
|
184
|
+
signing_key:
|
184
185
|
specification_version: 4
|
185
186
|
summary: Advisory locking for ActiveRecord
|
186
187
|
test_files:
|
@@ -189,7 +190,9 @@ test_files:
|
|
189
190
|
- test/lock_test.rb
|
190
191
|
- test/minitest_helper.rb
|
191
192
|
- test/nesting_test.rb
|
193
|
+
- test/options_test.rb
|
192
194
|
- test/parallelism_test.rb
|
195
|
+
- test/shared_test.rb
|
193
196
|
- test/test_models.rb
|
194
197
|
- test/thread_test.rb
|
195
|
-
|
198
|
+
- test/transaction_test.rb
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# This file was generated by Appraisal
|
2
|
-
|
3
|
-
source "https://rubygems.org"
|
4
|
-
|
5
|
-
gem "activerecord", "~> 4.1.0"
|
6
|
-
|
7
|
-
platforms :ruby do
|
8
|
-
gem "mysql2"
|
9
|
-
gem "pg"
|
10
|
-
gem "sqlite3"
|
11
|
-
end
|
12
|
-
|
13
|
-
platforms :jruby do
|
14
|
-
gem "activerecord-jdbcmysql-adapter"
|
15
|
-
gem "activerecord-jdbcpostgresql-adapter"
|
16
|
-
gem "activerecord-jdbcsqlite3-adapter"
|
17
|
-
end
|
18
|
-
|
19
|
-
gemspec :path => "../"
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# This file was generated by Appraisal
|
2
|
-
|
3
|
-
source "https://rubygems.org"
|
4
|
-
|
5
|
-
gem "activerecord", :github => "rails/rails"
|
6
|
-
gem "arel", :github => "rails/arel"
|
7
|
-
|
8
|
-
platforms :ruby do
|
9
|
-
gem "mysql2"
|
10
|
-
gem "pg"
|
11
|
-
gem "sqlite3"
|
12
|
-
end
|
13
|
-
|
14
|
-
platforms :jruby do
|
15
|
-
gem "activerecord-jdbcmysql-adapter"
|
16
|
-
gem "activerecord-jdbcpostgresql-adapter"
|
17
|
-
gem "activerecord-jdbcsqlite3-adapter"
|
18
|
-
end
|
19
|
-
|
20
|
-
gemspec :path => "../"
|