clasp 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5885cf34377db5819f7ede1186985edd13fb5159
4
- data.tar.gz: 3a64a4c1e0dfeee866cb52615ed6acb44fc48bd7
3
+ metadata.gz: 3b554152379fc07df7a3c60574f69bacfbfb5ddf
4
+ data.tar.gz: 3bdb8d2bc7b29bfc37967f740fe6cd503ca7bea2
5
5
  SHA512:
6
- metadata.gz: 046ca81ef9150e7cd3e77ada1f8f27d84dae98941b1cec86728381f74c46478c987163bf438ae3e0743b6951d7fc29ccd17d2ba94058e7c3465b3bb88e8698b2
7
- data.tar.gz: ecc4612154e5011d650c99a1e6bab07a1e67ef28a08f0ed9d85597c415cf42bd15f20bae0b6f6b3f9f7f6a24beed9d8e0dcc136eb4e0f547386812cf536d0955
6
+ metadata.gz: 2170cd98557f990ae3b6eb25c82e3db9fdf9a69d5a98619a9bf9b2653a3b90acd6fb195b7740e5e89a425db354728a6945935a16d90f4fb88e1d5d4ddc2090a8
7
+ data.tar.gz: 34b46a0aef1dfdedfa4989b3f6f32a3bd6d78107560c2c256b2df366de8e240f2651f604643dd8bd67c1df6f9c2d7474fa8647d035b406d9f6b54e6da34c149f
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Clasp
2
2
 
3
- [![Build Status](https://travis-ci.org/ianunruh/clasp.png?branch=master)](https://travis-ci.org/ianunruh/clasp)
3
+ [![Build Status](https://travis-ci.org/ianunruh/clasp.png?branch=master)](https://travis-ci.org/ianunruh/clasp) [![Gem Version](https://badge.fury.io/rb/clasp.png)](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
- ## Todo
41
+ ### Debugging deadlocks
42
42
 
43
- + More tests
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
+ ```
@@ -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
- 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
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
@@ -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) { DisposableLock.new }
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)
@@ -1,3 +1,3 @@
1
1
  module Clasp
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -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
- it 'detects a deadlock between two threads' do
53
- latch = CountdownLatch.new(2)
54
- deadlock = CountdownLatch.new(1)
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
- start_thread(latch, deadlock, lock_a, subject, lock_b, subject)
57
- start_thread(latch, deadlock, lock_b, subject, lock_a, subject)
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
- unless deadlock.await(30)
60
- raise 'Could not resolve deadlock within 30 seconds'
65
+ join_all
61
66
  end
62
- end
63
67
 
64
- it 'detects a deadlock between three threads in a vector' do
65
- latch = CountdownLatch.new(3)
66
- deadlock = CountdownLatch.new(1)
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
- 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)
76
+ unless deadlock.await(30)
77
+ raise 'Could not resolve deadlock within 30 seconds'
78
+ end
71
79
 
72
- unless deadlock.await(30)
73
- raise 'Could not resolve deadlock within 30 seconds'
80
+ join_all
74
81
  end
75
- end
76
82
 
77
- it 'detects a deadlock across lock managers' do
78
- manager_a = described_class.new
79
- manager_b = described_class.new
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
- latch = CountdownLatch.new(2)
82
- deadlock = CountdownLatch.new(1)
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
- 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)
93
+ unless deadlock.await(30)
94
+ raise 'Could not resolve deadlock within 30 seconds'
95
+ end
86
96
 
87
- unless deadlock.await(30)
88
- raise 'Could not resolve deadlock within 30 seconds'
97
+ join_all
89
98
  end
90
- end
91
99
 
92
- private
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
- 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
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
- begin
100
- latch.await
111
+ unless deadlock.await(30)
112
+ raise 'Could not resolve deadlock within 30 seconds'
113
+ end
101
114
 
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)
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.1.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