clasp 0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +43 -0
- data/lib/clasp/disposable_lock.rb +91 -0
- data/lib/clasp/errors.rb +10 -0
- data/lib/clasp/lock_manager.rb +60 -0
- data/lib/clasp/lock_manager_tracker.rb +24 -0
- data/lib/clasp/reentrant_lock.rb +166 -0
- data/lib/clasp/version.rb +3 -0
- data/lib/clasp.rb +13 -0
- data/spec/lock_manager_spec.rb +111 -0
- data/spec/reentrant_lock_spec.rb +49 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/countdown_latch.rb +24 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5885cf34377db5819f7ede1186985edd13fb5159
|
4
|
+
data.tar.gz: 3a64a4c1e0dfeee866cb52615ed6acb44fc48bd7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 046ca81ef9150e7cd3e77ada1f8f27d84dae98941b1cec86728381f74c46478c987163bf438ae3e0743b6951d7fc29ccd17d2ba94058e7c3465b3bb88e8698b2
|
7
|
+
data.tar.gz: ecc4612154e5011d650c99a1e6bab07a1e67ef28a08f0ed9d85597c415cf42bd15f20bae0b6f6b3f9f7f6a24beed9d8e0dcc136eb4e0f547386812cf536d0955
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Ian Unruh
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# Clasp
|
2
|
+
|
3
|
+
[](https://travis-ci.org/ianunruh/clasp)
|
4
|
+
|
5
|
+
Named reentrant locks with deadlock detection for Ruby
|
6
|
+
|
7
|
+
## Installation & usage
|
8
|
+
|
9
|
+
Install using `gem install clasp` or add it to your `Gemfile`.
|
10
|
+
|
11
|
+
### Basic usage
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
manager = Clasp::LockManager.new
|
15
|
+
|
16
|
+
manager.lock 'id1'
|
17
|
+
manager.unlock 'id1'
|
18
|
+
```
|
19
|
+
|
20
|
+
### Deadlock detection
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
## Thread 1 ## Thread 2
|
24
|
+
manager.lock 'AAA' manager.lock 'BBB'
|
25
|
+
begin begin
|
26
|
+
manager.lock 'BBB' manager.lock 'AAA'
|
27
|
+
# do work... # do work...
|
28
|
+
manager.unlock 'BBB' manager.unlock 'AAA'
|
29
|
+
ensure ensure
|
30
|
+
manager.unlock 'AAA' manager.unlock 'BBB'
|
31
|
+
end end
|
32
|
+
```
|
33
|
+
|
34
|
+
Using standard locks, this program could run forever because of a deadlock. Using `LockManager`
|
35
|
+
prevents this by tracking all locks and checking if a deadlock is about to occur.
|
36
|
+
|
37
|
+
The first thread that detects the deadlock can release any locks it holds. After this happens,
|
38
|
+
it can rollback and try again later. The competing thread will then be able to lock any
|
39
|
+
resources it needs to continue.
|
40
|
+
|
41
|
+
## Todo
|
42
|
+
|
43
|
+
+ More tests
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Clasp
|
2
|
+
# @api private
|
3
|
+
class DisposableLock
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :@lock, :owned?, :owned_by?, :queue
|
7
|
+
|
8
|
+
# @return [undefined]
|
9
|
+
def initialize
|
10
|
+
@closed = false
|
11
|
+
@lock = ReentrantLock.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Boolean]
|
15
|
+
def lock
|
16
|
+
unless @lock.try_lock
|
17
|
+
loop do
|
18
|
+
check_for_deadlock
|
19
|
+
break if @lock.try_timed_lock(0.3)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if @closed
|
24
|
+
@lock.unlock
|
25
|
+
false
|
26
|
+
else
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [undefined]
|
32
|
+
def unlock
|
33
|
+
@lock.unlock
|
34
|
+
ensure
|
35
|
+
try_close
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean]
|
39
|
+
def closed?
|
40
|
+
@closed
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @return [undefined]
|
46
|
+
def try_close
|
47
|
+
if @lock.try_lock
|
48
|
+
@closed = @lock.hold_count == 1
|
49
|
+
@lock.unlock
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @raise [DeadlockError]
|
54
|
+
# @return [undefined]
|
55
|
+
def check_for_deadlock
|
56
|
+
if @lock.locked? && !@lock.owned?
|
57
|
+
deadlock_candidates_for(Thread.current).each do |waiter|
|
58
|
+
if @lock.owned_by?(waiter)
|
59
|
+
raise DeadlockError, 'Imminent deadlock detected while acquiring a lock'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [Thread] thread
|
66
|
+
# @return [Set]
|
67
|
+
def deadlock_candidates_for(thread)
|
68
|
+
waiters_for_locks_owned_by(thread, LockManagerTracker.instances, Set.new)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param [Thread] thread
|
72
|
+
# @param [Enumerable] managers
|
73
|
+
# @param [Set] waiters
|
74
|
+
# @return [Set]
|
75
|
+
def waiters_for_locks_owned_by(thread, managers, waiters)
|
76
|
+
# Find all locks owned by the given thread
|
77
|
+
locks = managers.flat_map(&:locks).find_all { |lock|
|
78
|
+
lock.owned_by?(thread)
|
79
|
+
}
|
80
|
+
|
81
|
+
locks.flat_map(&:queue).each do |waiter|
|
82
|
+
if waiters.add?(waiter)
|
83
|
+
# Recursively find waiters for locks owned by this waiter
|
84
|
+
waiters_for_locks_owned_by(waiter, managers, waiters)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
waiters
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/clasp/errors.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Clasp
|
2
|
+
# Raised when an error occurs while acquiring a lock
|
3
|
+
class LockAcquisitionError < RuntimeError; end
|
4
|
+
|
5
|
+
# Raised when an imminent deadlock is detected while acquiring a lock
|
6
|
+
class DeadlockError < LockAcquisitionError; end
|
7
|
+
|
8
|
+
# Raised when a lock is used incorrectly by the calling thread
|
9
|
+
class IllegalLockUsageError < RuntimeError; end
|
10
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Clasp
|
2
|
+
class LockManager
|
3
|
+
# @return [undefined]
|
4
|
+
def initialize
|
5
|
+
@locks = ThreadSafe::Cache.new
|
6
|
+
LockManagerTracker.register(self)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Obtains the lock for the given identifier
|
10
|
+
#
|
11
|
+
# @raise [LockAcquisitonError]
|
12
|
+
# If the lock could not be acquired, usually due to deadlock
|
13
|
+
# @param [Object] identifier
|
14
|
+
# @return [undefined]
|
15
|
+
def lock(identifier)
|
16
|
+
obtained = false
|
17
|
+
until obtained
|
18
|
+
lock = @locks.compute_if_absent(identifier) { DisposableLock.new }
|
19
|
+
obtained = lock.lock
|
20
|
+
unless obtained
|
21
|
+
@locks.delete_pair(identifier, lock)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if the calling thread holds the lock for the given identifier
|
27
|
+
#
|
28
|
+
# @param [Object] identifier
|
29
|
+
# @return [Boolean]
|
30
|
+
def owned?(identifier)
|
31
|
+
lock = @locks.get(identifier)
|
32
|
+
lock && lock.owned?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Releases the lock for the given identifier
|
36
|
+
#
|
37
|
+
# @raise [IllegalLockUsageError]
|
38
|
+
# If the calling thread did not hold the lock for the given identifier
|
39
|
+
# @param [Object] identifier
|
40
|
+
# @return [undefined]
|
41
|
+
def unlock(identifier)
|
42
|
+
unless @locks.key?(identifier)
|
43
|
+
raise IllegalLockUsageError
|
44
|
+
end
|
45
|
+
|
46
|
+
lock = @locks.get(identifier)
|
47
|
+
lock.unlock
|
48
|
+
|
49
|
+
if lock.closed?
|
50
|
+
@locks.delete_pair(identifier, lock)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
# @return [Enumerable]
|
56
|
+
def locks
|
57
|
+
@locks.values
|
58
|
+
end
|
59
|
+
end # LockManager
|
60
|
+
end # Clasp
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Clasp
|
2
|
+
# @api private
|
3
|
+
module LockManagerTracker
|
4
|
+
extend self
|
5
|
+
|
6
|
+
@mutex = Mutex.new
|
7
|
+
@instances = Ref::WeakKeyMap.new
|
8
|
+
|
9
|
+
# @param [LockManager] instance
|
10
|
+
# @return [undefined]
|
11
|
+
def register(instance)
|
12
|
+
@mutex.synchronize do
|
13
|
+
@instances[instance] = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Enumerable]
|
18
|
+
def instances
|
19
|
+
@mutex.synchronize do
|
20
|
+
@instances.keys
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end # LockManagerTracker
|
24
|
+
end # Clasp
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Clasp
|
2
|
+
# Simple reentrant lock implementation that uses fair ordering
|
3
|
+
class ReentrantLock
|
4
|
+
# @return [Thread]
|
5
|
+
attr_reader :owner
|
6
|
+
|
7
|
+
# @return [undefined]
|
8
|
+
def initialize
|
9
|
+
@hold_count = 0
|
10
|
+
@mutex = Mutex.new
|
11
|
+
@queue = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the depth of the lock
|
15
|
+
# @return [Integer]
|
16
|
+
def hold_count
|
17
|
+
if @owner == Thread.current
|
18
|
+
@hold_count
|
19
|
+
else
|
20
|
+
0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a snapshot of the threads waiting for this lock
|
25
|
+
# @return [Enumerable]
|
26
|
+
def queue
|
27
|
+
@mutex.synchronize do
|
28
|
+
@queue.to_a
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the number of threads waiting for this lock
|
33
|
+
# @return [Integer]
|
34
|
+
def queue_size
|
35
|
+
@mutex.synchronize do
|
36
|
+
@queue.size
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if this lock is currently held by a thread
|
41
|
+
# @return [Boolean]
|
42
|
+
def locked?
|
43
|
+
!!@owner
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true if this lock is currently held by the calling thread
|
47
|
+
# @return [Boolean]
|
48
|
+
def owned?
|
49
|
+
@owner == Thread.current
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns true if this lock is currently held by the given thread
|
53
|
+
#
|
54
|
+
# @param [Thread] thread
|
55
|
+
# @return [Boolean]
|
56
|
+
def owned_by?(thread)
|
57
|
+
@owner == thread
|
58
|
+
end
|
59
|
+
|
60
|
+
# Acquires this lock for the calling thread
|
61
|
+
#
|
62
|
+
# If this lock has already been acquired by the calling thread, the hold count
|
63
|
+
# or depth of the lock is incremented.
|
64
|
+
#
|
65
|
+
# If this lock has already been acquired by another thread, this thread is placed
|
66
|
+
# in a queue and will be given the lock once all threads before it have gone
|
67
|
+
# through the queue.
|
68
|
+
#
|
69
|
+
# @return [undefined]
|
70
|
+
def lock
|
71
|
+
@mutex.synchronize do
|
72
|
+
unless @owner == Thread.current
|
73
|
+
while @owner
|
74
|
+
@queue.push Thread.current
|
75
|
+
|
76
|
+
begin
|
77
|
+
@mutex.sleep
|
78
|
+
ensure
|
79
|
+
@queue.pop
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
@owner = Thread.current
|
84
|
+
end
|
85
|
+
|
86
|
+
@hold_count += 1
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Attempts to acquire this lock for the calling thread
|
91
|
+
#
|
92
|
+
# If this lock has already been acquired by the calling thread, the hold count
|
93
|
+
# or depth of the lock is incremented.
|
94
|
+
#
|
95
|
+
# If this lock has already been acquired by another thread, this method will return
|
96
|
+
# immediately without waiting for the lock to become free.
|
97
|
+
#
|
98
|
+
# @return [Boolean]
|
99
|
+
def try_lock
|
100
|
+
@mutex.synchronize do
|
101
|
+
unless @owner == Thread.current
|
102
|
+
return false if @owner
|
103
|
+
@owner = Thread.current
|
104
|
+
end
|
105
|
+
|
106
|
+
@hold_count += 1
|
107
|
+
true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param [Float] timeout
|
112
|
+
# @return [Boolean]
|
113
|
+
def try_timed_lock(timeout)
|
114
|
+
@mutex.synchronize do
|
115
|
+
unless @owner == Thread.current
|
116
|
+
start = Time.now
|
117
|
+
|
118
|
+
while @owner
|
119
|
+
return false if (Time.now - start) >= timeout
|
120
|
+
|
121
|
+
@queue.push Thread.current
|
122
|
+
begin
|
123
|
+
@mutex.sleep timeout
|
124
|
+
ensure
|
125
|
+
@queue.pop
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
@owner = Thread.current
|
130
|
+
end
|
131
|
+
|
132
|
+
@hold_count += 1
|
133
|
+
true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# @raise [IllegalLockUsageError]
|
138
|
+
# @return [undefined]
|
139
|
+
def unlock
|
140
|
+
@mutex.synchronize do
|
141
|
+
unless @owner == Thread.current
|
142
|
+
raise IllegalLockUsageError, 'Calling thread not holding lock'
|
143
|
+
end
|
144
|
+
|
145
|
+
@hold_count -= 1
|
146
|
+
if @hold_count == 0
|
147
|
+
@owner = nil
|
148
|
+
wakeup_next
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# Invocation of this method must be synchronized
|
156
|
+
# @return [undefined]
|
157
|
+
def wakeup_next
|
158
|
+
t = @queue.first
|
159
|
+
t.wakeup if t
|
160
|
+
rescue ThreadError
|
161
|
+
# The thread died while waiting for the lock
|
162
|
+
@queue.pop
|
163
|
+
retry
|
164
|
+
end
|
165
|
+
end # ReentrantLock
|
166
|
+
end # Clasp
|
data/lib/clasp.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'atomic'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'ref'
|
4
|
+
require 'set'
|
5
|
+
require 'thread_safe'
|
6
|
+
|
7
|
+
require 'clasp/version'
|
8
|
+
|
9
|
+
require 'clasp/disposable_lock'
|
10
|
+
require 'clasp/errors'
|
11
|
+
require 'clasp/lock_manager'
|
12
|
+
require 'clasp/lock_manager_tracker'
|
13
|
+
require 'clasp/reentrant_lock'
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Clasp::LockManager do
|
4
|
+
let(:lock_a) { SecureRandom.uuid }
|
5
|
+
let(:lock_b) { SecureRandom.uuid }
|
6
|
+
let(:lock_c) { SecureRandom.uuid }
|
7
|
+
let(:lock_d) { SecureRandom.uuid }
|
8
|
+
|
9
|
+
before do
|
10
|
+
Thread.abort_on_exception = true
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'stores locks by identifiers' do
|
14
|
+
subject.should_not be_owned(lock_a)
|
15
|
+
subject.should_not be_owned(lock_b)
|
16
|
+
|
17
|
+
subject.lock(lock_a)
|
18
|
+
subject.should be_owned(lock_a)
|
19
|
+
subject.should_not be_owned(lock_b)
|
20
|
+
|
21
|
+
subject.unlock(lock_a)
|
22
|
+
subject.should_not be_owned(lock_a)
|
23
|
+
subject.should_not be_owned(lock_b)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'cleans up locks that are no longer in use' do
|
27
|
+
subject.locks.should be_empty
|
28
|
+
|
29
|
+
subject.lock(lock_a)
|
30
|
+
subject.unlock(lock_a)
|
31
|
+
|
32
|
+
subject.locks.should be_empty
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'keeps locks that are still in use' do
|
36
|
+
subject.lock(lock_a)
|
37
|
+
subject.lock(lock_a)
|
38
|
+
|
39
|
+
subject.unlock(lock_a)
|
40
|
+
subject.locks.should_not be_empty
|
41
|
+
|
42
|
+
subject.unlock(lock_a)
|
43
|
+
subject.locks.should be_empty
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'raises an exception when an unknown lock is released' do
|
47
|
+
expect {
|
48
|
+
subject.unlock(lock_a)
|
49
|
+
}.to raise_error(Clasp::IllegalLockUsageError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'detects a deadlock between two threads' do
|
53
|
+
latch = CountdownLatch.new(2)
|
54
|
+
deadlock = CountdownLatch.new(1)
|
55
|
+
|
56
|
+
start_thread(latch, deadlock, lock_a, subject, lock_b, subject)
|
57
|
+
start_thread(latch, deadlock, lock_b, subject, lock_a, subject)
|
58
|
+
|
59
|
+
unless deadlock.await(30)
|
60
|
+
raise 'Could not resolve deadlock within 30 seconds'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'detects a deadlock between three threads in a vector' do
|
65
|
+
latch = CountdownLatch.new(3)
|
66
|
+
deadlock = CountdownLatch.new(1)
|
67
|
+
|
68
|
+
start_thread(latch, deadlock, lock_a, subject, lock_b, subject)
|
69
|
+
start_thread(latch, deadlock, lock_b, subject, lock_c, subject)
|
70
|
+
start_thread(latch, deadlock, lock_c, subject, lock_a, subject)
|
71
|
+
|
72
|
+
unless deadlock.await(30)
|
73
|
+
raise 'Could not resolve deadlock within 30 seconds'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'detects a deadlock across lock managers' do
|
78
|
+
manager_a = described_class.new
|
79
|
+
manager_b = described_class.new
|
80
|
+
|
81
|
+
latch = CountdownLatch.new(2)
|
82
|
+
deadlock = CountdownLatch.new(1)
|
83
|
+
|
84
|
+
start_thread(latch, deadlock, lock_a, manager_a, lock_a, manager_b)
|
85
|
+
start_thread(latch, deadlock, lock_a, manager_b, lock_a, manager_a)
|
86
|
+
|
87
|
+
unless deadlock.await(30)
|
88
|
+
raise 'Could not resolve deadlock within 30 seconds'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def start_thread(latch, deadlock, lock_a, manager_a, lock_b, manager_b)
|
95
|
+
Thread.new do
|
96
|
+
manager_a.lock(lock_a)
|
97
|
+
latch.countdown
|
98
|
+
|
99
|
+
begin
|
100
|
+
latch.await
|
101
|
+
|
102
|
+
manager_b.lock(lock_b)
|
103
|
+
manager_b.unlock(lock_b)
|
104
|
+
rescue Clasp::DeadlockError
|
105
|
+
deadlock.countdown
|
106
|
+
ensure
|
107
|
+
manager_a.unlock(lock_a)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Clasp::ReentrantLock do
|
4
|
+
it 'exposes hold count differently depending on calling thread' do
|
5
|
+
subject.hold_count.should == 0
|
6
|
+
|
7
|
+
start_latch = CountdownLatch.new(1)
|
8
|
+
latch = CountdownLatch.new(1)
|
9
|
+
|
10
|
+
t = Thread.new do
|
11
|
+
subject.lock
|
12
|
+
subject.should be_owned
|
13
|
+
subject.hold_count.should == 1
|
14
|
+
|
15
|
+
start_latch.countdown
|
16
|
+
latch.await
|
17
|
+
|
18
|
+
subject.unlock
|
19
|
+
end
|
20
|
+
|
21
|
+
start_latch.await
|
22
|
+
|
23
|
+
subject.should be_owned_by(t)
|
24
|
+
subject.hold_count.should == 0
|
25
|
+
|
26
|
+
latch.countdown
|
27
|
+
|
28
|
+
t.join
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'raises an exception when released by non-owner thread' do
|
32
|
+
expect {
|
33
|
+
subject.unlock
|
34
|
+
}.to raise_error(Clasp::IllegalLockUsageError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'removes timed out waiters from the queue' do
|
38
|
+
subject.lock
|
39
|
+
|
40
|
+
t1 = Thread.new do
|
41
|
+
subject.try_timed_lock(0)
|
42
|
+
subject.queue_size.should == 0
|
43
|
+
end
|
44
|
+
|
45
|
+
t1.join
|
46
|
+
|
47
|
+
subject.unlock
|
48
|
+
end
|
49
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
class CountdownLatch
|
2
|
+
def initialize(initial)
|
3
|
+
@count = initial
|
4
|
+
|
5
|
+
@mutex = Mutex.new
|
6
|
+
@condition = ConditionVariable.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def countdown
|
10
|
+
@mutex.synchronize do
|
11
|
+
@count -= 1 if @count > 0
|
12
|
+
@condition.broadcast if @count == 0
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def await(timeout = nil)
|
17
|
+
@mutex.synchronize do
|
18
|
+
return true if @count == 0
|
19
|
+
@condition.wait @mutex, timeout
|
20
|
+
|
21
|
+
@count == 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clasp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian Unruh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2013-12-08 00:00:00 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: atomic
|
16
|
+
prerelease: false
|
17
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.14
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: *id001
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: ref
|
26
|
+
prerelease: false
|
27
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ~>
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 1.0.5
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id002
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: thread_safe
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.1.3
|
42
|
+
type: :runtime
|
43
|
+
version_requirements: *id003
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: rake
|
46
|
+
prerelease: false
|
47
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- &id005
|
50
|
+
- ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
type: :development
|
54
|
+
version_requirements: *id004
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
prerelease: false
|
58
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- *id005
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id006
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: simplecov
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- *id005
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id007
|
71
|
+
description: Identifier-based locking with deadlock detection
|
72
|
+
email: ianunruh@gmail.com
|
73
|
+
executables: []
|
74
|
+
|
75
|
+
extensions: []
|
76
|
+
|
77
|
+
extra_rdoc_files: []
|
78
|
+
|
79
|
+
files:
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- lib/clasp.rb
|
83
|
+
- lib/clasp/disposable_lock.rb
|
84
|
+
- lib/clasp/errors.rb
|
85
|
+
- lib/clasp/lock_manager.rb
|
86
|
+
- lib/clasp/lock_manager_tracker.rb
|
87
|
+
- lib/clasp/reentrant_lock.rb
|
88
|
+
- lib/clasp/version.rb
|
89
|
+
- spec/lock_manager_spec.rb
|
90
|
+
- spec/reentrant_lock_spec.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
- spec/support/countdown_latch.rb
|
93
|
+
homepage: https://github.com/ianunruh/clasp
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- *id005
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- *id005
|
109
|
+
requirements: []
|
110
|
+
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.1.10
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Identifier-based locking with deadlock detection
|
116
|
+
test_files:
|
117
|
+
- spec/lock_manager_spec.rb
|
118
|
+
- spec/reentrant_lock_spec.rb
|
119
|
+
- spec/spec_helper.rb
|
120
|
+
- spec/support/countdown_latch.rb
|