quack_concurrency 0.5.4 → 0.6.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.
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