with_advisory_lock 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -3
- data/README.md +20 -1
- data/lib/with_advisory_lock/base.rb +44 -27
- data/lib/with_advisory_lock/concern.rb +10 -9
- data/lib/with_advisory_lock/database_adapter_support.rb +0 -6
- data/lib/with_advisory_lock/mysql.rb +6 -11
- data/lib/with_advisory_lock/postgresql.rb +9 -6
- data/lib/with_advisory_lock/version.rb +1 -1
- data/test/lock_test.rb +8 -8
- data/test/minitest_helper.rb +12 -8
- data/test/parallelism_test.rb +30 -70
- data/test/thread_test.rb +60 -0
- data/tests.sh +3 -3
- data/with_advisory_lock.gemspec +2 -1
- metadata +55 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aeb99a0ddec77a51eeb59a41a26411a25390c65b
|
4
|
+
data.tar.gz: 3ee6f67835cbd6f3fe515f41b8134478ef2102ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58919a4b30f793b056f8c9f2af7f06f40673f5e9e6ef68213760f49b348f9044087ab16a1b398619e7c47bcba14db7eecadaf50a6f5652bfa5e36bb0ef1d9fc7
|
7
|
+
data.tar.gz: a819c9b4d0c2607505b90f9dbb8f981c041c3608ae69a63e7dac621d105479be268b5d425f1ec946c225b36175d595ed2115782a9a239cd3ad671c6e3b25d665
|
data/.travis.yml
CHANGED
@@ -23,10 +23,9 @@ before_script:
|
|
23
23
|
- mysql -e 'create database with_advisory_lock_test'
|
24
24
|
- psql -c 'create database with_advisory_lock_test' -U postgres
|
25
25
|
|
26
|
-
addons:
|
27
|
-
postgresql: "9.3"
|
28
|
-
|
29
26
|
matrix:
|
30
27
|
allow_failures:
|
31
28
|
- gemfile: gemfiles/activerecord_edge.gemfile
|
29
|
+
- rvm: jruby-19mode # travis' version of jruby has issues. Tests pass with jruby 1.7.13/java 1.8.0_11 on mac.
|
32
30
|
- rvm: rbx-2
|
31
|
+
|
data/README.md
CHANGED
@@ -41,9 +41,14 @@ 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.
|
42
42
|
|
43
43
|
Note that if a non-nil value is provided for `timeout_seconds`, the block will not be invoked if
|
44
|
-
the lock cannot be acquired within that
|
44
|
+
the lock cannot be acquired within that time-frame.
|
45
45
|
|
46
46
|
### Return values
|
47
|
+
|
48
|
+
The return value of `with_advisory_lock_result` is a `WithAdvisoryLock::Result` instance,
|
49
|
+
which has a `lock_was_acquired?` method and a `result` accessor method, which is
|
50
|
+
the returned value of the given block. If your block may validly return false, you should use
|
51
|
+
this method.
|
47
52
|
|
48
53
|
The return value of ```with_advisory_lock``` will be the result of the yielded block,
|
49
54
|
if the lock was able to be acquired and the block yielded, or ```false```, if you provided
|
@@ -134,6 +139,20 @@ end
|
|
134
139
|
|
135
140
|
## Changelog
|
136
141
|
|
142
|
+
### 3.0.0
|
143
|
+
|
144
|
+
* Added jruby/PostgreSQL support for Rails 4.x
|
145
|
+
* Reworked threaded tests to allow jruby tests to pass
|
146
|
+
|
147
|
+
#### API changes
|
148
|
+
|
149
|
+
* `yield_with_lock_and_timeout` and `yield_with_lock` now return instances of
|
150
|
+
`WithAdvisoryLock::Result`, so blocks that return `false` are not misinterpreted
|
151
|
+
as a failure to lock. As this changes the interface (albeit internal methods), the major version
|
152
|
+
number was incremented.
|
153
|
+
* `with_advisory_lock_result` was introduced, which clarifies whether the lock was acquired
|
154
|
+
versus the yielded block returned false.
|
155
|
+
|
137
156
|
### 2.0.0
|
138
157
|
|
139
158
|
* Lock timeouts of 0 now attempt the lock once, as per suggested by
|
@@ -1,6 +1,21 @@
|
|
1
1
|
require 'zlib'
|
2
2
|
|
3
3
|
module WithAdvisoryLock
|
4
|
+
class Result
|
5
|
+
attr_reader :result
|
6
|
+
|
7
|
+
def initialize(lock_was_acquired, result = false)
|
8
|
+
@lock_was_acquired = lock_was_acquired
|
9
|
+
@result = result
|
10
|
+
end
|
11
|
+
|
12
|
+
def lock_was_acquired?
|
13
|
+
@lock_was_acquired
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
FAILED_TO_LOCK = Result.new(false)
|
18
|
+
|
4
19
|
class Base
|
5
20
|
attr_reader :connection, :lock_name, :timeout_seconds
|
6
21
|
|
@@ -15,20 +30,22 @@ module WithAdvisoryLock
|
|
15
30
|
end
|
16
31
|
|
17
32
|
def self.lock_stack
|
33
|
+
# access doesn't need to be synchronized as it is only accessed by the current thread.
|
18
34
|
Thread.current[:with_advisory_lock_stack] ||= []
|
19
35
|
end
|
20
|
-
|
21
36
|
delegate :lock_stack, to: 'self.class'
|
22
37
|
|
23
38
|
def already_locked?
|
24
39
|
lock_stack.include? lock_str
|
25
40
|
end
|
26
41
|
|
27
|
-
def with_advisory_lock_if_needed
|
42
|
+
def with_advisory_lock_if_needed(&block)
|
28
43
|
if already_locked?
|
29
|
-
yield
|
44
|
+
Result.new(true, yield)
|
45
|
+
elsif timeout_seconds == 0
|
46
|
+
yield_with_lock(&block)
|
30
47
|
else
|
31
|
-
|
48
|
+
yield_with_lock_and_timeout(&block)
|
32
49
|
end
|
33
50
|
end
|
34
51
|
|
@@ -42,35 +59,35 @@ module WithAdvisoryLock
|
|
42
59
|
end
|
43
60
|
end
|
44
61
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
|
62
|
+
def yield_with_lock_and_timeout(&block)
|
63
|
+
give_up_at = Time.now + @timeout_seconds if @timeout_seconds
|
64
|
+
while @timeout_seconds.nil? || Time.now < give_up_at do
|
65
|
+
r = yield_with_lock(&block)
|
66
|
+
return r if r.lock_was_acquired?
|
67
|
+
# Randomizing sleep time may help reduce contention.
|
68
|
+
sleep(rand(0.05..0.15))
|
69
|
+
end
|
70
|
+
FAILED_TO_LOCK
|
49
71
|
end
|
50
72
|
|
51
73
|
def yield_with_lock
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
release_lock
|
61
|
-
end
|
62
|
-
else
|
63
|
-
# sleep between 1/20 and ~1/5 of a second.
|
64
|
-
# Randomizing sleep time may help reduce contention.
|
65
|
-
sleep(rand * 0.15 + 0.05)
|
74
|
+
if try_lock
|
75
|
+
begin
|
76
|
+
lock_stack.push(lock_str)
|
77
|
+
result = block_given? ? yield : nil
|
78
|
+
Result.new(true, result)
|
79
|
+
ensure
|
80
|
+
lock_stack.pop
|
81
|
+
release_lock
|
66
82
|
end
|
67
|
-
|
68
|
-
|
83
|
+
else
|
84
|
+
FAILED_TO_LOCK
|
85
|
+
end
|
69
86
|
end
|
70
87
|
|
71
|
-
#
|
72
|
-
def
|
73
|
-
"
|
88
|
+
# Prevent AR from caching results improperly
|
89
|
+
def unique_column_name
|
90
|
+
"t#{SecureRandom.hex}"
|
74
91
|
end
|
75
92
|
end
|
76
93
|
end
|
@@ -1,23 +1,24 @@
|
|
1
|
-
# Tried desperately to monkeypatch the polymorphic connection object,
|
2
|
-
# but rails autoloading is too clever by half. Pull requests are welcome.
|
3
|
-
|
4
1
|
require 'active_support/concern'
|
5
2
|
|
6
3
|
module WithAdvisoryLock
|
7
4
|
module Concern
|
8
5
|
extend ActiveSupport::Concern
|
9
|
-
|
10
6
|
delegate :with_advisory_lock, :advisory_lock_exists?, to: 'self.class'
|
11
7
|
|
12
8
|
module ClassMethods
|
13
9
|
def with_advisory_lock(lock_name, timeout_seconds=nil, &block)
|
10
|
+
result = with_advisory_lock_result(lock_name, timeout_seconds, &block)
|
11
|
+
result.lock_was_acquired? ? result.result : false
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_advisory_lock_result(lock_name, timeout_seconds=nil, &block)
|
14
15
|
impl = impl_class.new(connection, lock_name, timeout_seconds)
|
15
16
|
impl.with_advisory_lock_if_needed(&block)
|
16
17
|
end
|
17
18
|
|
18
19
|
def advisory_lock_exists?(lock_name)
|
19
20
|
impl = impl_class.new(connection, lock_name, 0)
|
20
|
-
impl.already_locked? || !impl.yield_with_lock
|
21
|
+
impl.already_locked? || !impl.yield_with_lock.lock_was_acquired?
|
21
22
|
end
|
22
23
|
|
23
24
|
def current_advisory_lock
|
@@ -27,12 +28,12 @@ module WithAdvisoryLock
|
|
27
28
|
private
|
28
29
|
|
29
30
|
def impl_class
|
30
|
-
|
31
|
-
|
31
|
+
adapter = WithAdvisoryLock::DatabaseAdapterSupport.new(connection)
|
32
|
+
if adapter.postgresql?
|
32
33
|
WithAdvisoryLock::PostgreSQL
|
33
|
-
|
34
|
+
elsif adapter.mysql?
|
34
35
|
WithAdvisoryLock::MySQL
|
35
|
-
else
|
36
|
+
else
|
36
37
|
WithAdvisoryLock::Flock
|
37
38
|
end
|
38
39
|
end
|
@@ -7,20 +7,15 @@ module WithAdvisoryLock
|
|
7
7
|
"MySQL doesn't support nested Advisory Locks",
|
8
8
|
lock_stack.dup)
|
9
9
|
end
|
10
|
-
#
|
11
|
-
# 0 if the attempt timed out (for example, because another client has
|
12
|
-
# previously locked the name), or NULL if an error occurred
|
13
|
-
# (such as running out of memory or the thread was killed with mysqladmin kill).
|
14
|
-
sql = "SELECT GET_LOCK(#{quoted_lock_str}, 0) #{query_cache_buster}"
|
15
|
-
connection.select_value(sql).to_i > 0
|
10
|
+
execute_successful?("GET_LOCK(#{quoted_lock_str}, 0)")
|
16
11
|
end
|
17
12
|
|
18
13
|
def release_lock
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
sql = "SELECT
|
14
|
+
execute_successful?("RELEASE_LOCK(#{quoted_lock_str})")
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute_successful?(mysql_function)
|
18
|
+
sql = "SELECT #{mysql_function} AS #{unique_column_name}"
|
24
19
|
connection.select_value(sql).to_i > 0
|
25
20
|
end
|
26
21
|
|
@@ -2,15 +2,18 @@ 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
|
-
|
6
|
-
# or return false if the lock cannot be acquired immediately
|
7
|
-
sql = "SELECT pg_try_advisory_lock(#{lock_keys.join(',')}) #{query_cache_buster}"
|
8
|
-
't' == connection.select_value(sql).to_s
|
5
|
+
execute_successful?('pg_try_advisory_lock')
|
9
6
|
end
|
10
7
|
|
11
8
|
def release_lock
|
12
|
-
|
13
|
-
|
9
|
+
execute_successful?('pg_advisory_unlock')
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute_successful?(pg_function)
|
13
|
+
sql = "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name}"
|
14
|
+
result = connection.select_value(sql)
|
15
|
+
# MRI returns 't', jruby returns true. YAY!
|
16
|
+
(result == 't' || result == true)
|
14
17
|
end
|
15
18
|
|
16
19
|
# PostgreSQL wants 2 32bit integers as the lock key.
|
data/test/lock_test.rb
CHANGED
@@ -1,22 +1,23 @@
|
|
1
1
|
require 'minitest_helper'
|
2
2
|
|
3
3
|
describe 'class methods' do
|
4
|
-
let(:lock_name) {
|
4
|
+
let(:lock_name) { 'test lock' }
|
5
5
|
|
6
6
|
describe '.current_advisory_lock' do
|
7
|
-
it
|
7
|
+
it 'returns nil outside an advisory lock request' do
|
8
8
|
Tag.current_advisory_lock.must_be_nil
|
9
9
|
end
|
10
10
|
|
11
11
|
it 'returns the name of the last lock acquired' do
|
12
12
|
Tag.with_advisory_lock(lock_name) do
|
13
|
+
# The lock name may have a prefix if WITH_ADVISORY_LOCK_PREFIX env is set
|
13
14
|
Tag.current_advisory_lock.must_match /#{lock_name}/
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
18
19
|
describe '.advisory_lock_exists?' do
|
19
|
-
it
|
20
|
+
it 'returns false for an unacquired lock' do
|
20
21
|
Tag.advisory_lock_exists?(lock_name).must_be_false
|
21
22
|
end
|
22
23
|
|
@@ -27,13 +28,12 @@ describe 'class methods' do
|
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
30
|
-
describe
|
31
|
+
describe 'zero timeout_seconds' do
|
31
32
|
it 'attempts the lock exactly once with no timeout' do
|
32
|
-
|
33
|
+
expected = SecureRandom.base64
|
33
34
|
Tag.with_advisory_lock(lock_name, 0) do
|
34
|
-
|
35
|
-
end
|
36
|
-
block_was_yielded.must_be_true
|
35
|
+
expected
|
36
|
+
end.must_equal expected
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
data/test/minitest_helper.rb
CHANGED
@@ -4,14 +4,14 @@ require 'with_advisory_lock'
|
|
4
4
|
require 'tmpdir'
|
5
5
|
require 'securerandom'
|
6
6
|
|
7
|
-
db_config = File.expand_path("database.yml", File.dirname(__FILE__))
|
8
|
-
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result)
|
9
|
-
|
10
7
|
def env_db
|
11
|
-
(ENV[
|
8
|
+
(ENV['DB'] || :mysql).to_sym
|
12
9
|
end
|
13
10
|
|
14
|
-
|
11
|
+
db_config = File.expand_path('database.yml', File.dirname(__FILE__))
|
12
|
+
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result)
|
13
|
+
|
14
|
+
ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex
|
15
15
|
|
16
16
|
ActiveRecord::Base.establish_connection(env_db)
|
17
17
|
ActiveRecord::Migration.verbose = false
|
@@ -19,14 +19,18 @@ ActiveRecord::Migration.verbose = false
|
|
19
19
|
require 'test_models'
|
20
20
|
begin
|
21
21
|
require 'minitest'
|
22
|
-
rescue LoadError
|
22
|
+
rescue LoadError
|
23
|
+
puts 'Failed to load the minitest gem; built-in version will be used.'
|
23
24
|
end
|
24
25
|
require 'minitest/autorun'
|
25
26
|
require 'minitest/great_expectations'
|
27
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
28
|
+
# minitest-reporters-1.0.5/lib/minitest/old_activesupport_fix.rb:7:in `remove_method': method `run' not defined in ActiveSupport::Testing::SetupAndTeardown::ForMinitest (NameError)
|
29
|
+
require 'minitest/reporters'
|
30
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
31
|
+
end
|
26
32
|
require 'mocha/setup'
|
27
33
|
|
28
|
-
Thread.abort_on_exception = true
|
29
|
-
|
30
34
|
class MiniTest::Spec
|
31
35
|
before do
|
32
36
|
ENV['FLOCK_DIR'] = Dir.mktmpdir
|
data/test/parallelism_test.rb
CHANGED
@@ -1,45 +1,52 @@
|
|
1
1
|
require 'minitest_helper'
|
2
|
+
require 'forwardable'
|
2
3
|
|
3
|
-
describe
|
4
|
+
describe 'parallelism' do
|
4
5
|
class FindOrCreateWorker
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@thread, :join, :wakeup, :join, :status, :to_s
|
8
|
+
|
9
|
+
def initialize(name, use_advisory_lock)
|
10
|
+
@name = name
|
11
|
+
@use_advisory_lock = use_advisory_lock
|
12
|
+
@thread = Thread.new { work_later }
|
13
|
+
end
|
14
|
+
|
15
|
+
def work_later
|
16
|
+
sleep
|
17
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
18
|
+
if @use_advisory_lock
|
19
|
+
Tag.with_advisory_lock(@name) { work }
|
20
|
+
else
|
21
|
+
work
|
14
22
|
end
|
15
23
|
end
|
16
24
|
end
|
17
25
|
|
18
|
-
def work
|
26
|
+
def work
|
19
27
|
Tag.transaction do
|
20
|
-
Tag.where(name: name).first_or_create
|
28
|
+
Tag.where(name: @name).first_or_create
|
21
29
|
end
|
22
30
|
end
|
23
|
-
|
24
|
-
def join
|
25
|
-
@thread.join
|
26
|
-
end
|
27
31
|
end
|
28
32
|
|
29
33
|
def run_workers
|
30
|
-
all_workers = []
|
31
34
|
@names = @iterations.times.map { |iter| "iteration ##{iter}" }
|
32
35
|
@names.each do |name|
|
33
|
-
wake_time = 1.second.from_now
|
34
36
|
workers = @workers.times.map do
|
35
|
-
FindOrCreateWorker.new(
|
37
|
+
FindOrCreateWorker.new(name, @use_advisory_lock)
|
38
|
+
end
|
39
|
+
# Wait for all the threads to get ready:
|
40
|
+
until workers.all? { |ea| ea.status == 'sleep' }
|
41
|
+
sleep(0.1)
|
36
42
|
end
|
43
|
+
# OK, GO!
|
44
|
+
workers.each(&:wakeup)
|
45
|
+
# Then wait for them to finish:
|
37
46
|
workers.each(&:join)
|
38
|
-
all_workers += workers
|
39
47
|
end
|
40
48
|
# Ensure we're still connected:
|
41
49
|
ActiveRecord::Base.connection_pool.connection
|
42
|
-
all_workers
|
43
50
|
end
|
44
51
|
|
45
52
|
before :each do
|
@@ -47,14 +54,14 @@ describe "parallelism" do
|
|
47
54
|
@workers = 10
|
48
55
|
end
|
49
56
|
|
50
|
-
it
|
57
|
+
it 'creates multiple duplicate rows without advisory locks' do
|
51
58
|
@use_advisory_lock = false
|
52
59
|
@iterations = 1
|
53
60
|
run_workers
|
54
61
|
Tag.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
55
62
|
TagAudit.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
56
63
|
Label.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
57
|
-
end unless env_db == :sqlite
|
64
|
+
end unless env_db == :sqlite # < SQLite, understandably, throws "The database file is locked (database is locked)"
|
58
65
|
|
59
66
|
it "doesn't create multiple duplicate rows with advisory locks" do
|
60
67
|
@use_advisory_lock = true
|
@@ -66,50 +73,3 @@ describe "parallelism" do
|
|
66
73
|
end
|
67
74
|
end
|
68
75
|
|
69
|
-
describe "separate thread tests" do
|
70
|
-
let(:lock_name) { "testing 1,2,3" }
|
71
|
-
|
72
|
-
before do
|
73
|
-
@t1_acquired_lock = false
|
74
|
-
@t1_return_value = nil
|
75
|
-
|
76
|
-
@t1 = Thread.new do
|
77
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
78
|
-
@t1_return_value = Label.with_advisory_lock(lock_name) do
|
79
|
-
t1_acquired_lock = true
|
80
|
-
sleep(0.4)
|
81
|
-
't1 finished'
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Wait for the thread to acquire the lock:
|
87
|
-
sleep(0.1)
|
88
|
-
ActiveRecord::Base.connection.reconnect!
|
89
|
-
end
|
90
|
-
|
91
|
-
after do
|
92
|
-
@t1.join
|
93
|
-
end
|
94
|
-
|
95
|
-
it "#with_advisory_lock with a 0 timeout returns false immediately" do
|
96
|
-
response = Label.with_advisory_lock(lock_name, 0) {}
|
97
|
-
response.must_be_false
|
98
|
-
end
|
99
|
-
|
100
|
-
it "#advisory_lock_exists? returns true when another thread has the lock" do
|
101
|
-
Tag.advisory_lock_exists?(lock_name).must_be_true
|
102
|
-
end
|
103
|
-
|
104
|
-
it "can re-establish the lock after the other thread releases it" do
|
105
|
-
@t1.join
|
106
|
-
@t1_return_value.must_equal 't1 finished'
|
107
|
-
|
108
|
-
# We should now be able to acquire the lock immediately:
|
109
|
-
reacquired = false
|
110
|
-
Label.with_advisory_lock(lock_name, 0) do
|
111
|
-
reacquired = true
|
112
|
-
end.must_be_true
|
113
|
-
reacquired.must_be_true
|
114
|
-
end
|
115
|
-
end
|
data/test/thread_test.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe 'separate thread tests' do
|
4
|
+
let(:lock_name) { 'testing 1,2,3' } # OMG COMMAS
|
5
|
+
|
6
|
+
before do
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@t1_acquired_lock = false
|
9
|
+
@t1_return_value = nil
|
10
|
+
|
11
|
+
@t1 = Thread.new do
|
12
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
13
|
+
@t1_return_value = Label.with_advisory_lock(lock_name) do
|
14
|
+
@mutex.synchronize { @t1_acquired_lock = true }
|
15
|
+
sleep
|
16
|
+
't1 finished'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Wait for the thread to acquire the lock:
|
22
|
+
until @mutex.synchronize { @t1_acquired_lock } do
|
23
|
+
sleep(0.1)
|
24
|
+
end
|
25
|
+
ActiveRecord::Base.connection.reconnect!
|
26
|
+
end
|
27
|
+
|
28
|
+
after do
|
29
|
+
@t1.wakeup if @t1.status == 'sleep'
|
30
|
+
@t1.join
|
31
|
+
end
|
32
|
+
|
33
|
+
it '#with_advisory_lock with a 0 timeout returns false immediately' do
|
34
|
+
response = Label.with_advisory_lock(lock_name, 0) do
|
35
|
+
fail 'should not be yielded to'
|
36
|
+
end
|
37
|
+
response.must_be_false
|
38
|
+
end
|
39
|
+
|
40
|
+
it '#with_advisory_lock yields to the provided block' do
|
41
|
+
@t1_acquired_lock.must_be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it '#advisory_lock_exists? returns true when another thread has the lock' do
|
45
|
+
Tag.advisory_lock_exists?(lock_name).must_be_true
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'can re-establish the lock after the other thread releases it' do
|
49
|
+
@t1.wakeup
|
50
|
+
@t1.join
|
51
|
+
@t1_return_value.must_equal 't1 finished'
|
52
|
+
|
53
|
+
# We should now be able to acquire the lock immediately:
|
54
|
+
reacquired = false
|
55
|
+
Label.with_advisory_lock(lock_name, 0) do
|
56
|
+
reacquired = true
|
57
|
+
end.must_be_true
|
58
|
+
reacquired.must_be_true
|
59
|
+
end
|
60
|
+
end
|
data/tests.sh
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/bin/bash -e
|
2
2
|
export DB
|
3
3
|
|
4
|
-
for RUBY in 2.1.2 jruby-1.7.
|
4
|
+
for RUBY in 2.1.2 jruby-1.7.13 ; do
|
5
5
|
rbenv local $RUBY
|
6
6
|
for DB in mysql postgresql sqlite ; do
|
7
7
|
echo "$DB | $(ruby -v)"
|
8
|
-
appraisal bundle update
|
9
|
-
appraisal rake test
|
8
|
+
# appraisal bundle update
|
9
|
+
appraisal rake test --verbose
|
10
10
|
done
|
11
11
|
done
|
data/with_advisory_lock.gemspec
CHANGED
@@ -19,11 +19,12 @@ Gem::Specification.new do |gem|
|
|
19
19
|
gem.require_paths = %w(lib)
|
20
20
|
|
21
21
|
gem.add_runtime_dependency 'activerecord', '>= 3.2'
|
22
|
+
gem.add_runtime_dependency 'thread_safe'
|
22
23
|
|
23
|
-
gem.add_development_dependency 'rake'
|
24
24
|
gem.add_development_dependency 'yard'
|
25
25
|
gem.add_development_dependency 'minitest'
|
26
26
|
gem.add_development_dependency 'minitest-great_expectations'
|
27
|
+
gem.add_development_dependency 'minitest-reporters'
|
27
28
|
gem.add_development_dependency 'mocha'
|
28
29
|
gem.add_development_dependency 'appraisal'
|
29
30
|
end
|
metadata
CHANGED
@@ -1,111 +1,125 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: with_advisory_lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.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: 2014-
|
11
|
+
date: 2014-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: activerecord
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
|
-
- -
|
16
|
+
- - '>='
|
18
17
|
- !ruby/object:Gem::Version
|
19
18
|
version: '3.2'
|
20
|
-
|
19
|
+
name: activerecord
|
21
20
|
prerelease: false
|
21
|
+
type: :runtime
|
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: rake
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
30
29
|
requirements:
|
31
|
-
- -
|
30
|
+
- - '>='
|
32
31
|
- !ruby/object:Gem::Version
|
33
32
|
version: '0'
|
34
|
-
|
33
|
+
name: thread_safe
|
35
34
|
prerelease: false
|
35
|
+
type: :runtime
|
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
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
44
43
|
requirements:
|
45
|
-
- -
|
44
|
+
- - '>='
|
46
45
|
- !ruby/object:Gem::Version
|
47
46
|
version: '0'
|
48
|
-
|
47
|
+
name: yard
|
49
48
|
prerelease: false
|
49
|
+
type: :development
|
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
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
58
57
|
requirements:
|
59
|
-
- -
|
58
|
+
- - '>='
|
60
59
|
- !ruby/object:Gem::Version
|
61
60
|
version: '0'
|
62
|
-
|
61
|
+
name: minitest
|
63
62
|
prerelease: false
|
63
|
+
type: :development
|
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
|
71
70
|
requirement: !ruby/object:Gem::Requirement
|
72
71
|
requirements:
|
73
|
-
- -
|
72
|
+
- - '>='
|
74
73
|
- !ruby/object:Gem::Version
|
75
74
|
version: '0'
|
76
|
-
|
75
|
+
name: minitest-great_expectations
|
77
76
|
prerelease: false
|
77
|
+
type: :development
|
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: mocha
|
85
84
|
requirement: !ruby/object:Gem::Requirement
|
86
85
|
requirements:
|
87
|
-
- -
|
86
|
+
- - '>='
|
88
87
|
- !ruby/object:Gem::Version
|
89
88
|
version: '0'
|
90
|
-
|
89
|
+
name: minitest-reporters
|
91
90
|
prerelease: false
|
91
|
+
type: :development
|
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: appraisal
|
99
98
|
requirement: !ruby/object:Gem::Requirement
|
100
99
|
requirements:
|
101
|
-
- -
|
100
|
+
- - '>='
|
102
101
|
- !ruby/object:Gem::Version
|
103
102
|
version: '0'
|
103
|
+
name: mocha
|
104
|
+
prerelease: false
|
104
105
|
type: :development
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
name: appraisal
|
105
118
|
prerelease: false
|
119
|
+
type: :development
|
106
120
|
version_requirements: !ruby/object:Gem::Requirement
|
107
121
|
requirements:
|
108
|
-
- -
|
122
|
+
- - '>='
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '0'
|
111
125
|
description: Advisory locking for ActiveRecord
|
@@ -115,8 +129,8 @@ executables: []
|
|
115
129
|
extensions: []
|
116
130
|
extra_rdoc_files: []
|
117
131
|
files:
|
118
|
-
-
|
119
|
-
-
|
132
|
+
- .gitignore
|
133
|
+
- .travis.yml
|
120
134
|
- Appraisals
|
121
135
|
- Gemfile
|
122
136
|
- LICENSE.txt
|
@@ -142,30 +156,31 @@ files:
|
|
142
156
|
- test/nesting_test.rb
|
143
157
|
- test/parallelism_test.rb
|
144
158
|
- test/test_models.rb
|
159
|
+
- test/thread_test.rb
|
145
160
|
- tests.sh
|
146
161
|
- with_advisory_lock.gemspec
|
147
162
|
homepage: https://github.com/mceachen/with_advisory_lock
|
148
163
|
licenses:
|
149
164
|
- MIT
|
150
165
|
metadata: {}
|
151
|
-
post_install_message:
|
166
|
+
post_install_message:
|
152
167
|
rdoc_options: []
|
153
168
|
require_paths:
|
154
169
|
- lib
|
155
170
|
required_ruby_version: !ruby/object:Gem::Requirement
|
156
171
|
requirements:
|
157
|
-
- -
|
172
|
+
- - '>='
|
158
173
|
- !ruby/object:Gem::Version
|
159
174
|
version: '0'
|
160
175
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
176
|
requirements:
|
162
|
-
- -
|
177
|
+
- - '>='
|
163
178
|
- !ruby/object:Gem::Version
|
164
179
|
version: '0'
|
165
180
|
requirements: []
|
166
|
-
rubyforge_project:
|
167
|
-
rubygems_version: 2.
|
168
|
-
signing_key:
|
181
|
+
rubyforge_project:
|
182
|
+
rubygems_version: 2.1.9
|
183
|
+
signing_key:
|
169
184
|
specification_version: 4
|
170
185
|
summary: Advisory locking for ActiveRecord
|
171
186
|
test_files:
|
@@ -176,4 +191,5 @@ test_files:
|
|
176
191
|
- test/nesting_test.rb
|
177
192
|
- test/parallelism_test.rb
|
178
193
|
- test/test_models.rb
|
179
|
-
|
194
|
+
- test/thread_test.rb
|
195
|
+
has_rdoc:
|