quack_concurrency 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/quack_concurrency.rb +6 -13
  4. data/lib/quack_concurrency/condition_variable.rb +91 -85
  5. data/lib/quack_concurrency/condition_variable/waitable.rb +108 -0
  6. data/lib/quack_concurrency/error.rb +1 -1
  7. data/lib/quack_concurrency/future.rb +31 -30
  8. data/lib/quack_concurrency/future/canceled.rb +1 -0
  9. data/lib/quack_concurrency/future/complete.rb +1 -0
  10. data/lib/quack_concurrency/mutex.rb +140 -38
  11. data/lib/quack_concurrency/queue.rb +32 -28
  12. data/lib/quack_concurrency/reentrant_mutex.rb +64 -76
  13. data/lib/quack_concurrency/safe_condition_variable.rb +23 -0
  14. data/lib/quack_concurrency/safe_condition_variable/waitable.rb +21 -0
  15. data/lib/quack_concurrency/safe_sleeper.rb +80 -0
  16. data/lib/quack_concurrency/sleeper.rb +100 -0
  17. data/lib/quack_concurrency/waiter.rb +32 -23
  18. data/spec/condition_variable_spec.rb +216 -0
  19. data/spec/future_spec.rb +145 -79
  20. data/spec/mutex_spec.rb +441 -0
  21. data/spec/queue_spec.rb +217 -77
  22. data/spec/reentrant_mutex_spec.rb +394 -99
  23. data/spec/safe_condition_variable_spec.rb +115 -0
  24. data/spec/safe_sleeper_spec.rb +197 -0
  25. data/spec/sleeper.rb +197 -0
  26. data/spec/waiter_spec.rb +181 -0
  27. metadata +16 -14
  28. data/lib/quack_concurrency/queue/error.rb +0 -6
  29. data/lib/quack_concurrency/reentrant_mutex/error.rb +0 -6
  30. data/lib/quack_concurrency/semaphore.rb +0 -139
  31. data/lib/quack_concurrency/semaphore/error.rb +0 -6
  32. data/lib/quack_concurrency/uninterruptible_condition_variable.rb +0 -94
  33. data/lib/quack_concurrency/uninterruptible_sleeper.rb +0 -81
  34. data/lib/quack_concurrency/yielder.rb +0 -35
  35. data/spec/semaphore_spec.rb +0 -244
@@ -0,0 +1,181 @@
1
+ require 'quack_concurrency'
2
+
3
+ describe QuackConcurrency::Waiter do
4
+
5
+ # arbitrary amount of time we will wait for everything to settle
6
+ def delay(units = 1)
7
+ sleep units
8
+ end
9
+
10
+ describe "::new" do
11
+
12
+ context "when called with no arguments" do
13
+ waiter = nil
14
+ it "should not raise error" do
15
+ expect{ waiter = QuackConcurrency::Waiter.new }.not_to raise_error
16
+ end
17
+ it "should return a Waiter" do
18
+ expect(waiter).to be_a(QuackConcurrency::Waiter)
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ describe "#any_waiting_threads?" do
25
+
26
+ context "when called with waiting threads" do
27
+ it "should return true" do
28
+ waiter = QuackConcurrency::Waiter.new
29
+ thread = Thread.new { waiter.wait }
30
+ delay(1)
31
+ expect(waiter.any_waiting_threads?).to be true
32
+ waiter.resume_all
33
+ thread.join
34
+ end
35
+ end
36
+
37
+ context "when called with no waiting threads" do
38
+ it "should return false" do
39
+ waiter = QuackConcurrency::Waiter.new
40
+ expect(waiter.any_waiting_threads?).to be false
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ describe "#resume_all" do
47
+
48
+ context "when called" do
49
+ it "should resume all threads currently waiting" do
50
+ waiter = QuackConcurrency::Waiter.new
51
+ thread1 = Thread.new { waiter.wait }
52
+ thread2 = Thread.new { waiter.wait }
53
+ delay(1)
54
+ waiter.resume_all
55
+ delay(1)
56
+ expect(thread1.alive?).to be false
57
+ expect(thread2.alive?).to be false
58
+ end
59
+ it "should not resume any future threads that call #wait" do
60
+ waiter = QuackConcurrency::Waiter.new
61
+ waiter.resume_all
62
+ thread = Thread.new { waiter.wait }
63
+ delay(1)
64
+ expect(thread.alive?).to be true
65
+ waiter.resume_all
66
+ thread.join
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ describe "#resume_all_indefinitely" do
73
+
74
+ context "when called" do
75
+ it "should resume all threads currently waiting" do
76
+ waiter = QuackConcurrency::Waiter.new
77
+ thread1 = Thread.new { waiter.wait }
78
+ thread2 = Thread.new { waiter.wait }
79
+ delay(1)
80
+ waiter.resume_all_indefinitely
81
+ delay(1)
82
+ expect(thread1.alive?).to be false
83
+ expect(thread2.alive?).to be false
84
+ end
85
+ it "should resume all future threads that call #wait" do
86
+ waiter = QuackConcurrency::Waiter.new
87
+ waiter.resume_all_indefinitely
88
+ thread1 = Thread.new { waiter.wait }
89
+ thread2 = Thread.new { waiter.wait }
90
+ delay(1)
91
+ expect(thread1.alive?).to be false
92
+ expect(thread2.alive?).to be false
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ describe "#resume_next" do
99
+
100
+ context "when called" do
101
+ it "should resume the next thread currently waiting" do
102
+ waiter = QuackConcurrency::Waiter.new
103
+ thread1 = Thread.new { waiter.wait }
104
+ delay(1)
105
+ thread2 = Thread.new { waiter.wait }
106
+ delay(1)
107
+ waiter.resume_next
108
+ delay(1)
109
+ expect(thread1.alive?).to be false
110
+ expect(thread2.alive?).to be true
111
+ waiter.resume_next
112
+ delay(1)
113
+ expect(thread2.alive?).to be false
114
+ end
115
+ end
116
+
117
+ end
118
+
119
+ describe "#wait" do
120
+
121
+ context "when called" do
122
+ it "should return only after one of the resume methods are called" do
123
+ waiter = QuackConcurrency::Waiter.new
124
+ thread1 = Thread.new { waiter.wait }
125
+ thread2 = Thread.new { waiter.wait }
126
+ delay(1)
127
+ expect(thread1.alive?).to be true
128
+ expect(thread2.alive?).to be true
129
+ waiter.resume_all
130
+ thread1.join
131
+ thread2.join
132
+ end
133
+ end
134
+
135
+ context "when called before Thread#run" do
136
+ it "should return only after one of the resume methods are called" do
137
+ waiter = QuackConcurrency::Waiter.new
138
+ elapsed_time = nil
139
+ thread = Thread.new { waiter.wait }
140
+ delay(1)
141
+ thread.run
142
+ delay(1)
143
+ expect(thread.alive?).to be true
144
+ waiter.resume_all
145
+ delay(1)
146
+ expect(thread.alive?).to be false
147
+ end
148
+ end
149
+
150
+ end
151
+
152
+ describe "#waiting_threads_count" do
153
+
154
+ context "when called" do
155
+ it "should return a Integer" do
156
+ waiter = QuackConcurrency::Waiter.new
157
+ expect(waiter.waiting_threads_count).to be_a(Integer)
158
+ end
159
+ end
160
+
161
+ context "when called with no waiting threads" do
162
+ it "should return 0" do
163
+ waiter = QuackConcurrency::Waiter.new
164
+ expect(waiter.waiting_threads_count).to be 0
165
+ end
166
+ end
167
+
168
+ context "when called with one waiting thread" do
169
+ it "should return 1" do
170
+ waiter = QuackConcurrency::Waiter.new
171
+ thread = Thread.new { waiter.wait }
172
+ delay(1)
173
+ expect(waiter.waiting_threads_count).to be 1
174
+ waiter.resume_next
175
+ thread.join
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quack_concurrency
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Fors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-18 00:00:00.000000000 Z
11
+ date: 2018-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: reentrant_mutex
@@ -25,8 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.1'
27
27
  description: Offers concurrency tools that could also be found in the 'Concurrent
28
- Ruby'. However, all these tools will also accept core class duck types to build
29
- off of.
28
+ Ruby'. However, all these tools will also accept core duck types to build off of.
30
29
  email: mail@robfors.com
31
30
  executables: []
32
31
  extensions: []
@@ -37,26 +36,29 @@ files:
37
36
  - Rakefile
38
37
  - lib/quack_concurrency.rb
39
38
  - lib/quack_concurrency/condition_variable.rb
39
+ - lib/quack_concurrency/condition_variable/waitable.rb
40
40
  - lib/quack_concurrency/error.rb
41
41
  - lib/quack_concurrency/future.rb
42
42
  - lib/quack_concurrency/future/canceled.rb
43
43
  - lib/quack_concurrency/future/complete.rb
44
44
  - lib/quack_concurrency/mutex.rb
45
45
  - lib/quack_concurrency/queue.rb
46
- - lib/quack_concurrency/queue/error.rb
47
46
  - lib/quack_concurrency/reentrant_mutex.rb
48
- - lib/quack_concurrency/reentrant_mutex/error.rb
49
- - lib/quack_concurrency/semaphore.rb
50
- - lib/quack_concurrency/semaphore/error.rb
51
- - lib/quack_concurrency/uninterruptible_condition_variable.rb
52
- - lib/quack_concurrency/uninterruptible_sleeper.rb
47
+ - lib/quack_concurrency/safe_condition_variable.rb
48
+ - lib/quack_concurrency/safe_condition_variable/waitable.rb
49
+ - lib/quack_concurrency/safe_sleeper.rb
50
+ - lib/quack_concurrency/sleeper.rb
53
51
  - lib/quack_concurrency/waiter.rb
54
- - lib/quack_concurrency/yielder.rb
52
+ - spec/condition_variable_spec.rb
55
53
  - spec/future_spec.rb
54
+ - spec/mutex_spec.rb
56
55
  - spec/queue_spec.rb
57
56
  - spec/reentrant_mutex_spec.rb
58
- - spec/semaphore_spec.rb
57
+ - spec/safe_condition_variable_spec.rb
58
+ - spec/safe_sleeper_spec.rb
59
+ - spec/sleeper.rb
59
60
  - spec/spec_helper.rb
61
+ - spec/waiter_spec.rb
60
62
  homepage: https://github.com/robfors/quack_concurrency
61
63
  licenses:
62
64
  - MIT
@@ -77,8 +79,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
79
  version: '0'
78
80
  requirements: []
79
81
  rubyforge_project:
80
- rubygems_version: 2.7.7
82
+ rubygems_version: 2.7.8
81
83
  signing_key:
82
84
  specification_version: 4
83
- summary: Concurrency tools that accept duck types of core classes.
85
+ summary: Concurrency tools with modifiable blocking behaviour.
84
86
  test_files: []
@@ -1,6 +0,0 @@
1
- module QuackConcurrency
2
- class Queue
3
- class Error < Error
4
- end
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- module QuackConcurrency
2
- class ReentrantMutex
3
- class Error < Error
4
- end
5
- end
6
- end
@@ -1,139 +0,0 @@
1
- # not ready yet
2
-
3
- module QuackConcurrency
4
- class Semaphore
5
-
6
- # Gets total permit count
7
- # @return [Integer]
8
- attr_reader :permit_count
9
-
10
- # Creates a new {Semaphore} concurrency tool.
11
- # @return [Semaphore]
12
- def initialize(permit_count = 1)
13
- raise 'not ready yet'
14
- @condition_variable = UninterruptibleConditionVariable.new
15
- verify_permit_count(permit_count)
16
- @permit_count = permit_count
17
- @permits_used = 0
18
- @mutex = ::ReentrantMutex.new
19
- end
20
-
21
- # Check if a permit is available to be released.
22
- # @return [Boolean]
23
- def permit_available?
24
- permits_available >= 1
25
- end
26
-
27
- # Counts number of permits available to be released.
28
- # @return [Integer]
29
- def permits_available
30
- @mutex.synchronize do
31
- raw_permits_available = @permit_count - @permits_used
32
- raw_permits_available.positive? ? raw_permits_available : 0
33
- end
34
- end
35
-
36
- # Returns a permit so it can be released again in the future.
37
- # @return [void]
38
- def reacquire
39
- @mutex.synchronize do
40
- raise Error, 'can not reacquire a permit, no permits released right now' if @permits_used == 0
41
- @permits_used -= 1
42
- @condition_variable.signal if permit_available?
43
- end
44
- nil
45
- end
46
-
47
- # Releases a permit.
48
- # @note Will block until a permit is available.
49
- # @return [void]
50
- def release
51
- @mutex.synchronize do
52
- @condition_variable.wait(@mutex) unless permit_available?
53
- raise 'internal error, invalid state' unless permit_available?
54
- @permits_used += 1
55
- end
56
- nil
57
- end
58
-
59
- # Changes the permit count after {Semaphore} has been created.
60
- # @raise [Error] if total permit count is reduced and not enough permits are available to remove
61
- # @return [void]
62
- def set_permit_count(new_permit_count)
63
- verify_permit_count(new_permit_count)
64
- @mutex.synchronize do
65
- remove_permits = @permit_count - new_permit_count
66
- if remove_permits.positive? && remove_permits > permits_available
67
- raise Error, 'can not set new permit count, not enough permits available to remove right now'
68
- end
69
- set_permit_count!(new_permit_count)
70
- end
71
- nil
72
- end
73
-
74
- # Changes the permit count after {Semaphore} has been created.
75
- # If total permit count is reduced and not enough permits are available to remove,
76
- # it will change the count anyway but some permits will need to be reacquired
77
- # before any can be released.
78
- # @return [void]
79
- def set_permit_count!(new_permit_count)
80
- verify_permit_count(new_permit_count)
81
- @mutex.synchronize do
82
- new_permits = new_permit_count - @permit_count
83
- if new_permits.positive?
84
- new_permits.times { add_permit }
85
- else
86
- remove_permits = -new_permits
87
- remove_permits.times { remove_permit! }
88
- end
89
- end
90
- nil
91
- end
92
-
93
- # Releases a permit, runs the block, and reacquires the permit when the block completes.
94
- # @return return value from yielded block
95
- def synchronize
96
- release
97
- begin
98
- yield
99
- ensure
100
- reacquire
101
- end
102
- end
103
-
104
- # Attempts to release a permit and returns immediately.
105
- # @return [Boolean] returns if the permit was released
106
- def try_release
107
- @mutex.synchronize do
108
- if permit_available?
109
- release
110
- true
111
- else
112
- false
113
- end
114
- end
115
- end
116
-
117
- private
118
-
119
- def add_permit
120
- @permit_count += 1
121
- @condition_variable.signal if permit_available?
122
- nil
123
- end
124
-
125
- def remove_permit!
126
- @permit_count -= 1
127
- raise 'internal error, invalid state' if @permit_count < 0
128
- nil
129
- end
130
-
131
- def verify_permit_count(permit_count)
132
- unless permit_count.is_a?(Integer) && permit_count >= 0
133
- raise ArgumentError, "'permit_count' must be a non negative Integer"
134
- end
135
- end
136
-
137
- end
138
- end
139
-
@@ -1,6 +0,0 @@
1
- module QuackConcurrency
2
- class Semaphore
3
- class Error < Error
4
- end
5
- end
6
- end
@@ -1,94 +0,0 @@
1
- module QuackConcurrency
2
-
3
- # Unlike `::ConditionVariable` {UninterruptibleConditionVariable} will
4
- # safely sleep a `Thread`s. Any calls to `Thread#run` directly, will be ignored.
5
- class UninterruptibleConditionVariable
6
-
7
- def initialize
8
- @waiting_threads_sleepers = []
9
- @mutex = ::Mutex.new
10
- end
11
-
12
- def any_waiting_threads?
13
- waiting_threads_count >= 1
14
- end
15
-
16
- def broadcast
17
- @mutex.synchronize do
18
- signal_next until @waiting_threads_sleepers.empty?
19
- end
20
- self
21
- end
22
-
23
- def signal
24
- @mutex.synchronize do
25
- signal_next if @waiting_threads_sleepers.any?
26
- end
27
- self
28
- end
29
-
30
- def wait(mutex = nil, timeout = nil)
31
- validate_mutex(mutex)
32
- if timeout != nil && !timeout.is_a?(Numeric)
33
- raise ArgumentError, "'timeout' argument must be nil or a Numeric"
34
- end
35
- sleeper = UninterruptibleSleeper.for_current
36
- @mutex.synchronize { @waiting_threads_sleepers.push(sleeper) }
37
- if mutex
38
- # ideally we would would check if this Thread can sleep (not the last Thread alive)
39
- # before we unlock the mutex, however I am not sure is that can be implemented
40
- if mutex.respond_to?(:unlock!)
41
- mutex.unlock! { sleep(sleeper, timeout) }
42
- else
43
- mutex.unlock
44
- begin
45
- sleep(sleeper, timeout)
46
- ensure # rescue a fatal error (eg. only Thread stopped)
47
- if mutex.locked?
48
- # another Thread locked this before it died
49
- # this is not a correct state to be in but I don't know how to fix it
50
- # given that there are no other alive Threads then than the ramifications should be minimal
51
- else
52
- mutex.lock
53
- end
54
- end
55
- end
56
- else
57
- sleep(sleeper, timeout)
58
- end
59
- self
60
- ensure
61
- @mutex.synchronize { @waiting_threads_sleepers.delete(sleeper) }
62
- end
63
-
64
- def waiting_threads_count
65
- @waiting_threads_sleepers.length
66
- end
67
-
68
- private
69
-
70
- # @api private
71
- def signal_next
72
- next_waiting_thread_sleeper = @waiting_threads_sleepers.shift
73
- next_waiting_thread_sleeper.run_thread if next_waiting_thread_sleeper
74
- nil
75
- end
76
-
77
- # @api private
78
- def sleep(sleeper, duration)
79
- if duration == nil || duration == Float::INFINITY
80
- sleeper.stop_thread
81
- else
82
- sleeper.sleep_thread(timeout)
83
- end
84
- nil
85
- end
86
-
87
- def validate_mutex(mutex)
88
- return if mutex == nil
89
- return if mutex.respond_to?(:lock) && (mutex.respond_to?(:unlock) || mutex.respond_to?(:unlock!))
90
- raise ArgumentError, "'mutex' must respond to 'lock' and ('unlock' or'unlock!')"
91
- end
92
-
93
- end
94
- end