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 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