clasp 0.1.0 → 0.2.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/README.md +25 -3
- data/lib/clasp.rb +14 -1
- data/lib/clasp/deadlock_session.rb +82 -0
- data/lib/clasp/disposable_lock.rb +17 -34
- data/lib/clasp/lock_manager.rb +4 -2
- data/lib/clasp/version.rb +1 -1
- data/spec/lock_manager_spec.rb +79 -44
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b554152379fc07df7a3c60574f69bacfbfb5ddf
|
4
|
+
data.tar.gz: 3bdb8d2bc7b29bfc37967f740fe6cd503ca7bea2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2170cd98557f990ae3b6eb25c82e3db9fdf9a69d5a98619a9bf9b2653a3b90acd6fb195b7740e5e89a425db354728a6945935a16d90f4fb88e1d5d4ddc2090a8
|
7
|
+
data.tar.gz: 34b46a0aef1dfdedfa4989b3f6f32a3bd6d78107560c2c256b2df366de8e240f2651f604643dd8bd67c1df6f9c2d7474fa8647d035b406d9f6b54e6da34c149f
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Clasp
|
2
2
|
|
3
|
-
[](https://travis-ci.org/ianunruh/clasp)
|
3
|
+
[](https://travis-ci.org/ianunruh/clasp) [](http://badge.fury.io/rb/clasp)
|
4
4
|
|
5
5
|
Named reentrant locks with deadlock detection for Ruby
|
6
6
|
|
@@ -38,6 +38,28 @@ The first thread that detects the deadlock can release any locks it holds. After
|
|
38
38
|
it can rollback and try again later. The competing thread will then be able to lock any
|
39
39
|
resources it needs to continue.
|
40
40
|
|
41
|
-
|
41
|
+
### Debugging deadlocks
|
42
42
|
|
43
|
-
|
43
|
+
Clasp provides advanced debugging information when deadlocks occur.
|
44
|
+
|
45
|
+
Turn on debugging mode for your application:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
Clasp.debug = true
|
49
|
+
```
|
50
|
+
|
51
|
+
Then log the messages for `Clasp::DeadlockError`. You could see informative details about how
|
52
|
+
the deadlock occured:
|
53
|
+
|
54
|
+
```
|
55
|
+
Imminent deadlock detected
|
56
|
+
|
57
|
+
#<Thread:0x62f0 id=6 run> wanted #<DisposableLock 25296/f2057ff3-1a65-4815-ac59-91d0d73f2c87>
|
58
|
+
#<Thread:0x62e4 id=7 sleep> is waiting on a lock owned by #<Thread:0x62f0 id=6 run>
|
59
|
+
|
60
|
+
Locks owned by #<Thread:0x62f0 id=6 run>
|
61
|
+
#<DisposableLock 25296/a8f0d33e-74db-45cd-867d-aa0ef82dfcf9>
|
62
|
+
|
63
|
+
Locks owned by #<Thread:0x62e4 id=7 sleep>
|
64
|
+
#<DisposableLock 25296/f2057ff3-1a65-4815-ac59-91d0d73f2c87>
|
65
|
+
```
|
data/lib/clasp.rb
CHANGED
@@ -6,8 +6,21 @@ require 'thread_safe'
|
|
6
6
|
|
7
7
|
require 'clasp/version'
|
8
8
|
|
9
|
+
require 'clasp/deadlock_session'
|
9
10
|
require 'clasp/disposable_lock'
|
10
11
|
require 'clasp/errors'
|
11
12
|
require 'clasp/lock_manager'
|
12
13
|
require 'clasp/lock_manager_tracker'
|
13
|
-
require 'clasp/reentrant_lock'
|
14
|
+
require 'clasp/reentrant_lock'
|
15
|
+
|
16
|
+
module Clasp
|
17
|
+
extend self
|
18
|
+
|
19
|
+
# Enable detailed information that can be used to diagnose deadlocks
|
20
|
+
# @return [Boolean]
|
21
|
+
attr_accessor :debug
|
22
|
+
|
23
|
+
alias_method :debug?, :debug
|
24
|
+
|
25
|
+
self.debug = false
|
26
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Clasp
|
2
|
+
class DeadlockSession
|
3
|
+
# @raise [DeadlockError]
|
4
|
+
# @param [Thread] thread
|
5
|
+
# @param [DisposableLock] lock
|
6
|
+
# @return [undefined]
|
7
|
+
def self.detect_for(thread, lock)
|
8
|
+
new.detect_for(thread, lock)
|
9
|
+
end
|
10
|
+
|
11
|
+
# @raise [DeadlockError]
|
12
|
+
# @param [Thread] thread
|
13
|
+
# @param [DisposableLock] lock
|
14
|
+
# @return [undefined]
|
15
|
+
def detect_for(thread, lock)
|
16
|
+
@managers = LockManagerTracker.instances
|
17
|
+
@waiters = Set.new
|
18
|
+
|
19
|
+
populate_waiters_for_threads_owned_by(thread)
|
20
|
+
|
21
|
+
@waiters.each do |waiter|
|
22
|
+
if lock.owned_by?(waiter)
|
23
|
+
on_deadlock_detected(thread, lock, waiter)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# @raise [DeadlockError]
|
31
|
+
# @param [Thread] thread
|
32
|
+
# @param [DisposableLock] lock
|
33
|
+
# @param [Thread] waiter
|
34
|
+
# @return [undefined]
|
35
|
+
def on_deadlock_detected(thread, lock, waiter)
|
36
|
+
if Clasp.debug?
|
37
|
+
message = "Imminent deadlock detected\n"
|
38
|
+
message << "\n"
|
39
|
+
message << "#{thread} wanted #{lock}\n"
|
40
|
+
message << "#{waiter} is waiting on a lock owned by #{thread}\n"
|
41
|
+
|
42
|
+
left_locks = locks_owned_by(thread)
|
43
|
+
right_locks = locks_owned_by(waiter)
|
44
|
+
|
45
|
+
message << "\n"
|
46
|
+
message << "Locks owned by #{thread}\n"
|
47
|
+
left_locks.each do |lock|
|
48
|
+
message << "\t#{lock}\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
message << "\n"
|
52
|
+
message << "Locks owned by #{waiter}\n"
|
53
|
+
right_locks.each do |lock|
|
54
|
+
message << "\t#{lock}\n"
|
55
|
+
end
|
56
|
+
else
|
57
|
+
message = "Imminent deadlock detected while acquiring lock"
|
58
|
+
end
|
59
|
+
|
60
|
+
raise DeadlockError, message
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param [Thread] thread
|
64
|
+
# @return [Enumerable]
|
65
|
+
def locks_owned_by(thread)
|
66
|
+
@managers.flat_map(&:locks).find_all { |lock|
|
67
|
+
lock.owned_by?(thread)
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param [Thread] thread
|
72
|
+
# @return [undefined]
|
73
|
+
def populate_waiters_for_threads_owned_by(thread)
|
74
|
+
locks_owned_by(thread).flat_map(&:queue).each do |waiter|
|
75
|
+
if @waiters.add?(waiter)
|
76
|
+
# Recursively find waiters for locks owned by this waiter
|
77
|
+
populate_waiters_for_threads_owned_by(waiter)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end # DeadlockSession
|
82
|
+
end # Clasp
|
@@ -5,10 +5,16 @@ module Clasp
|
|
5
5
|
|
6
6
|
def_delegators :@lock, :owned?, :owned_by?, :queue
|
7
7
|
|
8
|
+
# @param [LockManager] manager
|
9
|
+
# @param [Object] identifier
|
8
10
|
# @return [undefined]
|
9
|
-
def initialize
|
11
|
+
def initialize(manager, identifier)
|
10
12
|
@closed = false
|
11
13
|
@lock = ReentrantLock.new
|
14
|
+
|
15
|
+
# Used for debugging purposes
|
16
|
+
@manager = manager.__id__
|
17
|
+
@identifier = identifier
|
12
18
|
end
|
13
19
|
|
14
20
|
# @return [Boolean]
|
@@ -40,6 +46,13 @@ module Clasp
|
|
40
46
|
@closed
|
41
47
|
end
|
42
48
|
|
49
|
+
# @return [String]
|
50
|
+
def to_s
|
51
|
+
str = "#<DisposableLock #{@manager}/#{@identifier}"
|
52
|
+
str << " closed" if @closed
|
53
|
+
str << ">"
|
54
|
+
end
|
55
|
+
|
43
56
|
private
|
44
57
|
|
45
58
|
# @return [undefined]
|
@@ -54,38 +67,8 @@ module Clasp
|
|
54
67
|
# @return [undefined]
|
55
68
|
def check_for_deadlock
|
56
69
|
if @lock.locked? && !@lock.owned?
|
57
|
-
|
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
|
70
|
+
DeadlockSession.detect_for(Thread.current, self)
|
86
71
|
end
|
87
|
-
|
88
|
-
waiters
|
89
72
|
end
|
90
|
-
end
|
91
|
-
end
|
73
|
+
end # DisposableLock
|
74
|
+
end # Clasp
|
data/lib/clasp/lock_manager.rb
CHANGED
@@ -15,7 +15,9 @@ module Clasp
|
|
15
15
|
def lock(identifier)
|
16
16
|
obtained = false
|
17
17
|
until obtained
|
18
|
-
lock = @locks.compute_if_absent(identifier) {
|
18
|
+
lock = @locks.compute_if_absent(identifier) {
|
19
|
+
DisposableLock.new(self, identifier)
|
20
|
+
}
|
19
21
|
obtained = lock.lock
|
20
22
|
unless obtained
|
21
23
|
@locks.delete_pair(identifier, lock)
|
@@ -40,7 +42,7 @@ module Clasp
|
|
40
42
|
# @return [undefined]
|
41
43
|
def unlock(identifier)
|
42
44
|
unless @locks.key?(identifier)
|
43
|
-
raise IllegalLockUsageError
|
45
|
+
raise IllegalLockUsageError, "Unknown lock #{identifier}"
|
44
46
|
end
|
45
47
|
|
46
48
|
lock = @locks.get(identifier)
|
data/lib/clasp/version.rb
CHANGED
data/spec/lock_manager_spec.rb
CHANGED
@@ -6,10 +6,6 @@ describe Clasp::LockManager do
|
|
6
6
|
let(:lock_c) { SecureRandom.uuid }
|
7
7
|
let(:lock_d) { SecureRandom.uuid }
|
8
8
|
|
9
|
-
before do
|
10
|
-
Thread.abort_on_exception = true
|
11
|
-
end
|
12
|
-
|
13
9
|
it 'stores locks by identifiers' do
|
14
10
|
subject.should_not be_owned(lock_a)
|
15
11
|
subject.should_not be_owned(lock_b)
|
@@ -49,63 +45,102 @@ describe Clasp::LockManager do
|
|
49
45
|
}.to raise_error(Clasp::IllegalLockUsageError)
|
50
46
|
end
|
51
47
|
|
52
|
-
|
53
|
-
|
54
|
-
|
48
|
+
context 'deadlock detection' do
|
49
|
+
let(:threads) { ThreadSafe::Array.new }
|
50
|
+
|
51
|
+
before { Thread.abort_on_exception = true }
|
52
|
+
after { Thread.abort_on_exception = false }
|
55
53
|
|
56
|
-
|
57
|
-
|
54
|
+
it 'detects a deadlock between two threads' do
|
55
|
+
latch = CountdownLatch.new(2)
|
56
|
+
deadlock = CountdownLatch.new(1)
|
57
|
+
|
58
|
+
start_thread(latch, deadlock, lock_a, subject, lock_b, subject)
|
59
|
+
start_thread(latch, deadlock, lock_b, subject, lock_a, subject)
|
60
|
+
|
61
|
+
unless deadlock.await(30)
|
62
|
+
raise 'Could not resolve deadlock within 30 seconds'
|
63
|
+
end
|
58
64
|
|
59
|
-
|
60
|
-
raise 'Could not resolve deadlock within 30 seconds'
|
65
|
+
join_all
|
61
66
|
end
|
62
|
-
end
|
63
67
|
|
64
|
-
|
65
|
-
|
66
|
-
|
68
|
+
it 'detects a deadlock between three threads in a vector' do
|
69
|
+
latch = CountdownLatch.new(3)
|
70
|
+
deadlock = CountdownLatch.new(1)
|
71
|
+
|
72
|
+
start_thread(latch, deadlock, lock_a, subject, lock_b, subject)
|
73
|
+
start_thread(latch, deadlock, lock_b, subject, lock_c, subject)
|
74
|
+
start_thread(latch, deadlock, lock_c, subject, lock_a, subject)
|
67
75
|
|
68
|
-
|
69
|
-
|
70
|
-
|
76
|
+
unless deadlock.await(30)
|
77
|
+
raise 'Could not resolve deadlock within 30 seconds'
|
78
|
+
end
|
71
79
|
|
72
|
-
|
73
|
-
raise 'Could not resolve deadlock within 30 seconds'
|
80
|
+
join_all
|
74
81
|
end
|
75
|
-
end
|
76
82
|
|
77
|
-
|
78
|
-
|
79
|
-
|
83
|
+
it 'detects a deadlock across lock managers' do
|
84
|
+
manager_a = described_class.new
|
85
|
+
manager_b = described_class.new
|
86
|
+
|
87
|
+
latch = CountdownLatch.new(2)
|
88
|
+
deadlock = CountdownLatch.new(1)
|
80
89
|
|
81
|
-
|
82
|
-
|
90
|
+
start_thread(latch, deadlock, lock_a, manager_a, lock_a, manager_b)
|
91
|
+
start_thread(latch, deadlock, lock_a, manager_b, lock_a, manager_a)
|
83
92
|
|
84
|
-
|
85
|
-
|
93
|
+
unless deadlock.await(30)
|
94
|
+
raise 'Could not resolve deadlock within 30 seconds'
|
95
|
+
end
|
86
96
|
|
87
|
-
|
88
|
-
raise 'Could not resolve deadlock within 30 seconds'
|
97
|
+
join_all
|
89
98
|
end
|
90
|
-
end
|
91
99
|
|
92
|
-
|
100
|
+
context 'with debugging mode enabled' do
|
101
|
+
before { Clasp.debug = true }
|
102
|
+
after { Clasp.debug = false }
|
103
|
+
|
104
|
+
it 'provides detailed information about deadlock' do
|
105
|
+
latch = CountdownLatch.new(2)
|
106
|
+
deadlock = CountdownLatch.new(1)
|
93
107
|
|
94
|
-
|
95
|
-
|
96
|
-
manager_a.lock(lock_a)
|
97
|
-
latch.countdown
|
108
|
+
start_thread(latch, deadlock, lock_a, subject, lock_b, subject)
|
109
|
+
start_thread(latch, deadlock, lock_b, subject, lock_a, subject)
|
98
110
|
|
99
|
-
|
100
|
-
|
111
|
+
unless deadlock.await(30)
|
112
|
+
raise 'Could not resolve deadlock within 30 seconds'
|
113
|
+
end
|
101
114
|
|
102
|
-
|
103
|
-
manager_b.unlock(lock_b)
|
104
|
-
rescue Clasp::DeadlockError
|
105
|
-
deadlock.countdown
|
106
|
-
ensure
|
107
|
-
manager_a.unlock(lock_a)
|
115
|
+
join_all
|
108
116
|
end
|
109
117
|
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def start_thread(latch, deadlock, lock_a, manager_a, lock_b, manager_b)
|
122
|
+
thread = Thread.new do
|
123
|
+
manager_a.lock(lock_a)
|
124
|
+
latch.countdown
|
125
|
+
|
126
|
+
begin
|
127
|
+
latch.await
|
128
|
+
|
129
|
+
manager_b.lock(lock_b)
|
130
|
+
manager_b.unlock(lock_b)
|
131
|
+
rescue Clasp::DeadlockError
|
132
|
+
@exception = $!
|
133
|
+
deadlock.countdown
|
134
|
+
ensure
|
135
|
+
manager_a.unlock(lock_a)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
threads.push(thread)
|
140
|
+
end
|
141
|
+
|
142
|
+
def join_all
|
143
|
+
threads.map(&:join)
|
144
|
+
end
|
110
145
|
end
|
111
146
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clasp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ian Unruh
|
@@ -80,6 +80,7 @@ files:
|
|
80
80
|
- LICENSE
|
81
81
|
- README.md
|
82
82
|
- lib/clasp.rb
|
83
|
+
- lib/clasp/deadlock_session.rb
|
83
84
|
- lib/clasp/disposable_lock.rb
|
84
85
|
- lib/clasp/errors.rb
|
85
86
|
- lib/clasp/lock_manager.rb
|