concurrent-ruby 0.3.2 → 0.4.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 +76 -225
- data/lib/concurrent.rb +18 -2
- data/lib/concurrent/actor.rb +180 -72
- data/lib/concurrent/channel.rb +28 -0
- data/lib/concurrent/dereferenceable.rb +21 -1
- data/lib/concurrent/event.rb +33 -1
- data/lib/concurrent/postable.rb +98 -0
- data/lib/concurrent/stoppable.rb +20 -0
- data/lib/concurrent/timer_task.rb +213 -8
- data/lib/concurrent/utilities.rb +12 -1
- data/lib/concurrent/version.rb +1 -1
- data/md/actor.md +2 -2
- data/md/agent.md +1 -1
- data/md/channel.md +40 -0
- data/md/dereferenceable.md +7 -9
- data/md/future.md +2 -2
- data/md/promise.md +1 -1
- data/md/scheduled_task.md +1 -1
- data/md/timer_task.md +1 -1
- data/spec/concurrent/actor_spec.rb +48 -255
- data/spec/concurrent/channel_spec.rb +86 -0
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +0 -18
- data/spec/concurrent/event_spec.rb +38 -3
- data/spec/concurrent/global_thread_pool_spec.rb +0 -19
- data/spec/concurrent/postable_shared.rb +222 -0
- data/spec/concurrent/stoppable_shared.rb +37 -0
- data/spec/concurrent/timer_task_spec.rb +37 -1
- metadata +22 -15
- data/lib/concurrent/goroutine.rb +0 -25
- data/md/goroutine.md +0 -54
- data/spec/concurrent/goroutine_spec.rb +0 -52
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative 'postable_shared'
|
3
|
+
require_relative 'runnable_shared'
|
4
|
+
require_relative 'stoppable_shared'
|
5
|
+
|
6
|
+
module Concurrent
|
7
|
+
|
8
|
+
describe Channel do
|
9
|
+
|
10
|
+
context :runnable do
|
11
|
+
subject{ Channel.new{ nil } }
|
12
|
+
it_should_behave_like :runnable
|
13
|
+
end
|
14
|
+
|
15
|
+
context :stoppable do
|
16
|
+
subject do
|
17
|
+
task = Channel.new{ nil }
|
18
|
+
task.run!
|
19
|
+
task
|
20
|
+
end
|
21
|
+
it_should_behave_like :stoppable
|
22
|
+
end
|
23
|
+
|
24
|
+
context :postable do
|
25
|
+
|
26
|
+
let!(:postable_class){ Channel }
|
27
|
+
|
28
|
+
let(:sender) do
|
29
|
+
Channel.new do |*message|
|
30
|
+
if message.first.is_a?(Exception)
|
31
|
+
raise message.first
|
32
|
+
else
|
33
|
+
message.first
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:receiver){ Channel.new{|*message| message.first } }
|
39
|
+
|
40
|
+
it_should_behave_like :postable
|
41
|
+
end
|
42
|
+
|
43
|
+
subject{ Channel.new{ nil } }
|
44
|
+
|
45
|
+
context '#initialize' do
|
46
|
+
|
47
|
+
it 'raises an exception if no block is given' do
|
48
|
+
expect {
|
49
|
+
Channel.new
|
50
|
+
}.to raise_error(ArgumentError)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context '#behavior' do
|
55
|
+
|
56
|
+
it 'calls the block once for each message' do
|
57
|
+
@expected = false
|
58
|
+
channel = Channel.new{ @expected = true }
|
59
|
+
channel.run!
|
60
|
+
channel << 42
|
61
|
+
sleep(0.1)
|
62
|
+
channel.stop
|
63
|
+
@expected.should be_true
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'passes all arguments to the block' do
|
67
|
+
@expected = []
|
68
|
+
channel = Channel.new{|*message| @expected = message }
|
69
|
+
channel.run!
|
70
|
+
channel.post(1,2,3,4,5)
|
71
|
+
sleep(0.1)
|
72
|
+
channel.stop
|
73
|
+
@expected.should eq [1,2,3,4,5]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context '#pool' do
|
78
|
+
|
79
|
+
it 'passes a duplicate of the block to each channel in the pool' do
|
80
|
+
block = proc{ nil }
|
81
|
+
block.should_receive(:dup).exactly(5).times.and_return(proc{ nil })
|
82
|
+
mbox, pool = Channel.pool(5, &block)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -2,7 +2,6 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
require 'concurrent/agent'
|
4
4
|
require 'concurrent/future'
|
5
|
-
require 'concurrent/goroutine'
|
6
5
|
require 'concurrent/promise'
|
7
6
|
|
8
7
|
if mri?
|
@@ -52,23 +51,6 @@ if mri?
|
|
52
51
|
|
53
52
|
context 'operation' do
|
54
53
|
|
55
|
-
context 'goroutine' do
|
56
|
-
|
57
|
-
it 'passes all arguments to the block' do
|
58
|
-
$GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
|
59
|
-
|
60
|
-
EventMachine.run do
|
61
|
-
|
62
|
-
@expected = nil
|
63
|
-
go(1, 2, 3){|a, b, c| @expected = [c, b, a] }
|
64
|
-
sleep(0.1)
|
65
|
-
@expected.should eq [3, 2, 1]
|
66
|
-
|
67
|
-
EventMachine.stop
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
54
|
context Agent do
|
73
55
|
|
74
56
|
subject { Agent.new(0) }
|
@@ -21,7 +21,7 @@ module Concurrent
|
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'returns false if the event is unset' do
|
24
|
-
subject.reset
|
24
|
+
#subject.reset
|
25
25
|
subject.should_not be_set
|
26
26
|
end
|
27
27
|
end
|
@@ -29,7 +29,7 @@ module Concurrent
|
|
29
29
|
context '#set' do
|
30
30
|
|
31
31
|
it 'triggers the event' do
|
32
|
-
subject.reset
|
32
|
+
#subject.reset
|
33
33
|
@expected = false
|
34
34
|
Thread.new{ subject.wait; @expected = true }
|
35
35
|
sleep(0.1)
|
@@ -46,12 +46,47 @@ module Concurrent
|
|
46
46
|
|
47
47
|
context '#reset' do
|
48
48
|
|
49
|
-
it '
|
49
|
+
it 'does not change the state of an unset event' do
|
50
|
+
subject.reset
|
51
|
+
subject.should_not be_set
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'does not trigger an unset event' do
|
55
|
+
@expected = false
|
56
|
+
Thread.new{ subject.wait; @expected = true }
|
57
|
+
sleep(0.1)
|
58
|
+
subject.reset
|
59
|
+
sleep(0.1)
|
60
|
+
@expected.should be_false
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'does not interrupt waiting threads when event is unset' do
|
64
|
+
@expected = false
|
65
|
+
Thread.new{ subject.wait; @expected = true }
|
66
|
+
sleep(0.1)
|
67
|
+
subject.reset
|
68
|
+
sleep(0.1)
|
69
|
+
subject.set
|
70
|
+
sleep(0.1)
|
71
|
+
@expected.should be_true
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns true when called on an unset event' do
|
75
|
+
subject.reset.should be_true
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'sets the state of a set event to unset' do
|
50
79
|
subject.set
|
51
80
|
subject.should be_set
|
52
81
|
subject.reset
|
53
82
|
subject.should_not be_set
|
54
83
|
end
|
84
|
+
|
85
|
+
it 'returns true when called on a set event' do
|
86
|
+
subject.set
|
87
|
+
subject.should be_set
|
88
|
+
subject.reset.should be_true
|
89
|
+
end
|
55
90
|
end
|
56
91
|
|
57
92
|
context '#wait' do
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'concurrent/goroutine'
|
3
2
|
require_relative 'uses_global_thread_pool_shared'
|
4
3
|
|
5
4
|
module Concurrent
|
@@ -62,23 +61,5 @@ module Concurrent
|
|
62
61
|
subject << proc{ nil }
|
63
62
|
end
|
64
63
|
end
|
65
|
-
|
66
|
-
context 'operation' do
|
67
|
-
|
68
|
-
context 'goroutine' do
|
69
|
-
|
70
|
-
it 'gets a new thread' do
|
71
|
-
$GLOBAL_THREAD_POOL = subject
|
72
|
-
|
73
|
-
t = Thread.new{ nil }
|
74
|
-
|
75
|
-
Thread.should_receive(:new).with(no_args()).and_return(t)
|
76
|
-
go{ nil }
|
77
|
-
|
78
|
-
Thread.should_receive(:new).with(1,2,3).and_return(t)
|
79
|
-
go(1,2,3){ nil }
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
64
|
end
|
84
65
|
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
share_examples_for :postable do
|
4
|
+
|
5
|
+
after(:each) do
|
6
|
+
subject.stop
|
7
|
+
@thread.kill unless @thread.nil?
|
8
|
+
sleep(0.1)
|
9
|
+
end
|
10
|
+
|
11
|
+
context '#post' do
|
12
|
+
|
13
|
+
it 'raises an exception when the message is empty' do
|
14
|
+
expect {
|
15
|
+
subject.post
|
16
|
+
}.to raise_error(ArgumentError)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns false when not running' do
|
20
|
+
subject.post(42).should be_false
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'pushes a message onto the queue' do
|
24
|
+
@expected = false
|
25
|
+
postable = postable_class.new{|msg| @expected = msg }
|
26
|
+
@thread = Thread.new{ postable.run }
|
27
|
+
@thread.join(0.1)
|
28
|
+
postable.post(true)
|
29
|
+
@thread.join(0.1)
|
30
|
+
@expected.should be_true
|
31
|
+
postable.stop
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns the current size of the queue' do
|
35
|
+
postable = postable_class.new{|msg| sleep }
|
36
|
+
@thread = Thread.new{ postable.run }
|
37
|
+
@thread.join(0.1)
|
38
|
+
postable.post(true).should == 1
|
39
|
+
@thread.join(0.1)
|
40
|
+
postable.post(true).should == 1
|
41
|
+
@thread.join(0.1)
|
42
|
+
postable.post(true).should == 2
|
43
|
+
postable.stop
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'is aliased a <<' do
|
47
|
+
@expected = false
|
48
|
+
postable = postable_class.new{|msg| @expected = msg }
|
49
|
+
@thread = Thread.new{ postable.run }
|
50
|
+
@thread.join(0.1)
|
51
|
+
postable << true
|
52
|
+
@thread.join(0.1)
|
53
|
+
@expected.should be_true
|
54
|
+
postable.stop
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context '#post?' do
|
59
|
+
|
60
|
+
it 'returns nil when not running' do
|
61
|
+
subject.post?(42).should be_false
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns an Obligation' do
|
65
|
+
postable = postable_class.new{ nil }
|
66
|
+
@thread = Thread.new{ postable.run }
|
67
|
+
@thread.join(0.1)
|
68
|
+
obligation = postable.post?(nil)
|
69
|
+
obligation.should be_a(Concurrent::Obligation)
|
70
|
+
postable.stop
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'fulfills the obligation on success' do
|
74
|
+
postable = postable_class.new{|msg| @expected = msg }
|
75
|
+
@thread = Thread.new{ postable.run }
|
76
|
+
@thread.join(0.1)
|
77
|
+
obligation = postable.post?(42)
|
78
|
+
@thread.join(0.1)
|
79
|
+
obligation.should be_fulfilled
|
80
|
+
obligation.value.should == 42
|
81
|
+
postable.stop
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'rejects the obligation on failure' do
|
85
|
+
postable = postable_class.new{|msg| raise StandardError.new('Boom!') }
|
86
|
+
@thread = Thread.new{ postable.run }
|
87
|
+
@thread.join(0.1)
|
88
|
+
obligation = postable.post?(42)
|
89
|
+
@thread.join(0.1)
|
90
|
+
obligation.should be_rejected
|
91
|
+
obligation.reason.should be_a(StandardError)
|
92
|
+
postable.stop
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context '#post!' do
|
97
|
+
|
98
|
+
it 'raises Concurrent::Runnable::LifecycleError when not running' do
|
99
|
+
expect {
|
100
|
+
subject.post!(1, 'Hello World!')
|
101
|
+
}.to raise_error(Concurrent::Runnable::LifecycleError)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'blocks for up to the given number of seconds' do
|
105
|
+
postable = postable_class.new{|msg| sleep }
|
106
|
+
@thread = Thread.new{ postable.run }
|
107
|
+
@thread.join(0.1)
|
108
|
+
start = Time.now.to_i
|
109
|
+
expect {
|
110
|
+
postable.post!(2, nil)
|
111
|
+
}.to raise_error
|
112
|
+
elapsed = Time.now.to_i - start
|
113
|
+
elapsed.should >= 2
|
114
|
+
postable.stop
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'raises Concurrent::TimeoutError when seconds is zero' do
|
118
|
+
postable = postable_class.new{|msg| 42 }
|
119
|
+
@thread = Thread.new{ postable.run }
|
120
|
+
@thread.join(0.1)
|
121
|
+
expect {
|
122
|
+
postable.post!(0, nil)
|
123
|
+
}.to raise_error(Concurrent::TimeoutError)
|
124
|
+
postable.stop
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'raises Concurrent::TimeoutError on timeout' do
|
128
|
+
postable = postable_class.new{|msg| sleep }
|
129
|
+
@thread = Thread.new{ postable.run }
|
130
|
+
@thread.join(0.1)
|
131
|
+
expect {
|
132
|
+
postable.post!(1, nil)
|
133
|
+
}.to raise_error(Concurrent::TimeoutError)
|
134
|
+
postable.stop
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'bubbles the exception on error' do
|
138
|
+
postable = postable_class.new{|msg| raise StandardError.new('Boom!') }
|
139
|
+
@thread = Thread.new{ postable.run }
|
140
|
+
@thread.join(0.1)
|
141
|
+
expect {
|
142
|
+
postable.post!(1, nil)
|
143
|
+
}.to raise_error(StandardError)
|
144
|
+
postable.stop
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'returns the result on success' do
|
148
|
+
postable = postable_class.new{|msg| 42 }
|
149
|
+
@thread = Thread.new{ postable.run }
|
150
|
+
@thread.join(0.1)
|
151
|
+
expected = postable.post!(1, nil)
|
152
|
+
expected.should == 42
|
153
|
+
postable.stop
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'attempts to cancel the operation on timeout' do
|
157
|
+
@expected = 0
|
158
|
+
postable = postable_class.new{|msg| sleep(0.5); @expected += 1 }
|
159
|
+
@thread = Thread.new{ postable.run }
|
160
|
+
@thread.join(0.1)
|
161
|
+
postable.post(nil) # block the postable
|
162
|
+
expect {
|
163
|
+
postable.post!(0.1, nil)
|
164
|
+
}.to raise_error(Concurrent::TimeoutError)
|
165
|
+
sleep(1.5)
|
166
|
+
@expected.should == 1
|
167
|
+
postable.stop
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context '#forward' do
|
172
|
+
|
173
|
+
let(:observer) { double('observer') }
|
174
|
+
|
175
|
+
before(:each) do
|
176
|
+
@sender = Thread.new{ sender.run }
|
177
|
+
@receiver = Thread.new{ receiver.run }
|
178
|
+
@sender.join(0.1)
|
179
|
+
@receiver.join(0.1)
|
180
|
+
end
|
181
|
+
|
182
|
+
after(:each) do
|
183
|
+
sender.stop
|
184
|
+
receiver.stop
|
185
|
+
sleep(0.1)
|
186
|
+
@sender.kill unless @sender.nil?
|
187
|
+
@receiver.kill unless @receiver.nil?
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'returns false when sender not running' do
|
191
|
+
sender.stop
|
192
|
+
sleep(0.1)
|
193
|
+
sender.forward(receiver, 'Hello World!').should be_false
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'forwards the result to the receiver on success' do
|
197
|
+
receiver.should_receive(:post).with(42)
|
198
|
+
sender.forward(receiver, 42)
|
199
|
+
sleep(0.1)
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'does not forward on exception' do
|
203
|
+
receiver.should_not_receive(:post).with(42)
|
204
|
+
sender.forward(receiver, StandardError.new)
|
205
|
+
sleep(0.1)
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'notifies observers on success' do
|
209
|
+
observer.should_receive(:update).with(any_args())
|
210
|
+
sender.add_observer(observer)
|
211
|
+
sender.forward(receiver, 42)
|
212
|
+
sleep(0.1)
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'notifies observers on exception' do
|
216
|
+
observer.should_not_receive(:update).with(any_args())
|
217
|
+
sender.add_observer(observer)
|
218
|
+
sender.forward(receiver, StandardError.new)
|
219
|
+
sleep(0.1)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
share_examples_for :stoppable do
|
4
|
+
|
5
|
+
after(:each) do
|
6
|
+
subject.stop
|
7
|
+
end
|
8
|
+
|
9
|
+
context 'stopping' do
|
10
|
+
|
11
|
+
it 'raises an exception when #before_stop does not receive a block' do
|
12
|
+
expect {
|
13
|
+
subject.before_stop
|
14
|
+
}.to raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'raises an exception if #before_stop is called more than once' do
|
18
|
+
subject.before_stop{ nil }
|
19
|
+
expect {
|
20
|
+
subject.before_stop{ nil }
|
21
|
+
}.to raise_error(Concurrent::Runnable::LifecycleError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns self from #before_stop' do
|
25
|
+
task = subject
|
26
|
+
task.before_stop{ nil }.should eq task
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'calls the #before_stop block when stopping' do
|
30
|
+
@expected = false
|
31
|
+
subject.before_stop{ @expected = true }
|
32
|
+
subject.stop
|
33
|
+
sleep(0.1)
|
34
|
+
@expected.should be_true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|