clasp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ [![Build Status](https://travis-ci.org/ianunruh/clasp.png?branch=master)](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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Clasp
2
+ VERSION = '0.1.0'
3
+ end
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
@@ -0,0 +1,8 @@
1
+ require 'securerandom'
2
+ require 'simplecov'
3
+
4
+ SimpleCov.start
5
+
6
+ require 'clasp'
7
+
8
+ require File.expand_path '../support/countdown_latch', __FILE__
@@ -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