concurrent-ruby 0.0.1 → 0.1.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 +223 -3
- data/lib/concurrent.rb +19 -0
- data/lib/concurrent/agent.rb +130 -0
- data/lib/concurrent/cached_thread_pool.rb +122 -0
- data/lib/concurrent/defer.rb +69 -0
- data/lib/concurrent/event.rb +60 -0
- data/lib/concurrent/event_machine_defer_proxy.rb +23 -0
- data/lib/concurrent/fixed_thread_pool.rb +89 -0
- data/lib/concurrent/future.rb +42 -0
- data/lib/concurrent/global_thread_pool.rb +3 -0
- data/lib/concurrent/goroutine.rb +25 -0
- data/lib/concurrent/obligation.rb +121 -0
- data/lib/concurrent/promise.rb +194 -0
- data/lib/concurrent/thread_pool.rb +61 -0
- data/lib/concurrent/version.rb +1 -1
- data/lib/concurrent_ruby.rb +1 -0
- data/md/agent.md +123 -0
- data/md/defer.md +174 -0
- data/md/event.md +32 -0
- data/md/future.md +83 -0
- data/md/goroutine.md +52 -0
- data/md/obligation.md +32 -0
- data/md/promise.md +225 -0
- data/md/thread_pool.md +197 -0
- data/spec/concurrent/agent_spec.rb +405 -0
- data/spec/concurrent/cached_thread_pool_spec.rb +112 -0
- data/spec/concurrent/defer_spec.rb +199 -0
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +246 -0
- data/spec/concurrent/event_spec.rb +134 -0
- data/spec/concurrent/fixed_thread_pool_spec.rb +84 -0
- data/spec/concurrent/future_spec.rb +115 -0
- data/spec/concurrent/goroutine_spec.rb +52 -0
- data/spec/concurrent/obligation_shared.rb +121 -0
- data/spec/concurrent/promise_spec.rb +310 -0
- data/spec/concurrent/thread_pool_shared.rb +209 -0
- data/spec/spec_helper.rb +2 -0
- metadata +61 -4
@@ -0,0 +1,405 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
describe Agent do
|
6
|
+
|
7
|
+
subject { Agent.new(0) }
|
8
|
+
|
9
|
+
let(:observer) do
|
10
|
+
Class.new do
|
11
|
+
attr_reader :value
|
12
|
+
define_method(:update) do |time, value|
|
13
|
+
@value = value
|
14
|
+
end
|
15
|
+
end.new
|
16
|
+
end
|
17
|
+
|
18
|
+
before(:each) do
|
19
|
+
$GLOBAL_THREAD_POOL = CachedThreadPool.new
|
20
|
+
end
|
21
|
+
|
22
|
+
context '#initialize' do
|
23
|
+
|
24
|
+
it 'sets the value to the given initial state' do
|
25
|
+
Agent.new(10).value.should eq 10
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sets the timeout to the given value' do
|
29
|
+
Agent.new(0, 5).timeout.should eq 5
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sets the timeout to the default when nil' do
|
33
|
+
Agent.new(0).timeout.should eq Agent::TIMEOUT
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets the length to zero' do
|
37
|
+
Agent.new(10).length.should eq 0
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'spawns the worker thread' do
|
41
|
+
$GLOBAL_THREAD_POOL.should_receive(:post).once.with(any_args())
|
42
|
+
Agent.new(0)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context '#rescue' do
|
47
|
+
|
48
|
+
it 'returns self when a block is given' do
|
49
|
+
a1 = subject
|
50
|
+
a2 = a1.rescue{}
|
51
|
+
a1.object_id.should eq a2.object_id
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'returns self when no block is given' do
|
55
|
+
a1 = subject
|
56
|
+
a2 = a1.rescue
|
57
|
+
a1.object_id.should eq a2.object_id
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'accepts an exception class as the first parameter' do
|
61
|
+
lambda {
|
62
|
+
subject.rescue(StandardError){}
|
63
|
+
}.should_not raise_error
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'ignores rescuers without a block' do
|
67
|
+
subject.rescue
|
68
|
+
subject.instance_variable_get(:@rescuers).should be_empty
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context '#validate' do
|
73
|
+
|
74
|
+
it 'returns self when a block is given' do
|
75
|
+
a1 = subject
|
76
|
+
a2 = a1.validate{}
|
77
|
+
a1.object_id.should eq a2.object_id
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'returns self when no block is given' do
|
81
|
+
a1 = subject
|
82
|
+
a2 = a1.validate
|
83
|
+
a1.object_id.should eq a2.object_id
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'ignores validators without a block' do
|
87
|
+
subject.validate
|
88
|
+
subject.instance_variable_get(:@validator).should be_nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context '#post' do
|
93
|
+
|
94
|
+
it 'adds the given block to the queue' do
|
95
|
+
before = subject.length
|
96
|
+
subject.post{ nil }
|
97
|
+
subject.post{ nil }
|
98
|
+
subject.length.should eq before+2
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'does not add to the queue when no block is given' do
|
102
|
+
before = subject.length
|
103
|
+
subject.post
|
104
|
+
subject.post{ nil }
|
105
|
+
subject.length.should eq before+1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context '#length' do
|
110
|
+
|
111
|
+
it 'should be zero for a new agent' do
|
112
|
+
subject.length.should eq 0
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should increase by one for each #post' do
|
116
|
+
subject.post{ sleep }
|
117
|
+
subject.post{ sleep }
|
118
|
+
subject.post{ sleep }
|
119
|
+
subject.length.should eq 3
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should decrease by one each time a handler is run' do
|
123
|
+
subject.post{ nil }
|
124
|
+
subject.post{ sleep }
|
125
|
+
subject.post{ sleep }
|
126
|
+
sleep(0.1)
|
127
|
+
subject.length.should eq 1
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'fulfillment' do
|
132
|
+
|
133
|
+
it 'process each block in the queue' do
|
134
|
+
@expected = []
|
135
|
+
subject.post{ @expected << 1 }
|
136
|
+
subject.post{ @expected << 2 }
|
137
|
+
subject.post{ @expected << 3 }
|
138
|
+
sleep(0.1)
|
139
|
+
@expected.should eq [1,2,3]
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'passes the current value to the handler' do
|
143
|
+
@expected = nil
|
144
|
+
Agent.new(10).post{|i| @expected = i }
|
145
|
+
sleep(0.1)
|
146
|
+
@expected.should eq 10
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'sets the value to the handler return value on success' do
|
150
|
+
subject.post{ 100 }
|
151
|
+
sleep(0.1)
|
152
|
+
subject.value.should eq 100
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'rejects the handler after timeout reached' do
|
156
|
+
agent = Agent.new(0, 0.1)
|
157
|
+
agent.post{ sleep(1); 10 }
|
158
|
+
agent.value.should eq 0
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'validation' do
|
163
|
+
|
164
|
+
it 'processes the validator when present' do
|
165
|
+
@expected = nil
|
166
|
+
subject.validate{ @expected = 10; true }
|
167
|
+
subject.post{ nil }
|
168
|
+
sleep(0.1)
|
169
|
+
@expected.should eq 10
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'passes the new value to the validator' do
|
173
|
+
@expected = nil
|
174
|
+
subject.validate{|v| @expected = v; true }
|
175
|
+
subject.post{ 10 }
|
176
|
+
sleep(0.1)
|
177
|
+
@expected.should eq 10
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'sets the new value when the validator returns true' do
|
181
|
+
agent = Agent.new(0).validate{ true }
|
182
|
+
agent.post{ 10 }
|
183
|
+
sleep(0.1)
|
184
|
+
agent.value.should eq 10
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'does not change the value when the validator returns false' do
|
188
|
+
agent = Agent.new(0).validate{ false }
|
189
|
+
agent.post{ 10 }
|
190
|
+
sleep(0.1)
|
191
|
+
agent.value.should eq 0
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'does not change the value when the validator raises an exception' do
|
195
|
+
agent = Agent.new(0).validate{ raise StandardError }
|
196
|
+
agent.post{ 10 }
|
197
|
+
sleep(0.1)
|
198
|
+
agent.value.should eq 0
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context 'rejection' do
|
203
|
+
|
204
|
+
it 'calls the first exception block with a matching class' do
|
205
|
+
@expected = nil
|
206
|
+
subject.
|
207
|
+
rescue(StandardError){|ex| @expected = 1 }.
|
208
|
+
rescue(StandardError){|ex| @expected = 2 }.
|
209
|
+
rescue(StandardError){|ex| @expected = 3 }
|
210
|
+
subject.post{ raise StandardError }
|
211
|
+
sleep(0.1)
|
212
|
+
@expected.should eq 1
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'matches all with a rescue with no class given' do
|
216
|
+
@expected = nil
|
217
|
+
subject.
|
218
|
+
rescue(LoadError){|ex| @expected = 1 }.
|
219
|
+
rescue{|ex| @expected = 2 }.
|
220
|
+
rescue(StandardError){|ex| @expected = 3 }
|
221
|
+
subject.post{ raise NoMethodError }
|
222
|
+
sleep(0.1)
|
223
|
+
@expected.should eq 2
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'searches associated rescue handlers in order' do
|
227
|
+
@expected = nil
|
228
|
+
subject.
|
229
|
+
rescue(ArgumentError){|ex| @expected = 1 }.
|
230
|
+
rescue(LoadError){|ex| @expected = 2 }.
|
231
|
+
rescue(Exception){|ex| @expected = 3 }
|
232
|
+
subject.post{ raise ArgumentError }
|
233
|
+
sleep(0.1)
|
234
|
+
@expected.should eq 1
|
235
|
+
|
236
|
+
@expected = nil
|
237
|
+
subject.
|
238
|
+
rescue(ArgumentError){|ex| @expected = 1 }.
|
239
|
+
rescue(LoadError){|ex| @expected = 2 }.
|
240
|
+
rescue(Exception){|ex| @expected = 3 }
|
241
|
+
subject.post{ raise LoadError }
|
242
|
+
sleep(0.1)
|
243
|
+
@expected.should eq 2
|
244
|
+
|
245
|
+
@expected = nil
|
246
|
+
subject.
|
247
|
+
rescue(ArgumentError){|ex| @expected = 1 }.
|
248
|
+
rescue(LoadError){|ex| @expected = 2 }.
|
249
|
+
rescue(Exception){|ex| @expected = 3 }
|
250
|
+
subject.post{ raise StandardError }
|
251
|
+
sleep(0.1)
|
252
|
+
@expected.should eq 3
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'passes the exception object to the matched block' do
|
256
|
+
@expected = nil
|
257
|
+
subject.
|
258
|
+
rescue(ArgumentError){|ex| @expected = ex }.
|
259
|
+
rescue(LoadError){|ex| @expected = ex }.
|
260
|
+
rescue(Exception){|ex| @expected = ex }
|
261
|
+
subject.post{ raise StandardError }
|
262
|
+
sleep(0.1)
|
263
|
+
@expected.should be_a(StandardError)
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'ignores rescuers without a block' do
|
267
|
+
@expected = nil
|
268
|
+
subject.
|
269
|
+
rescue(StandardError).
|
270
|
+
rescue(StandardError){|ex| @expected = ex }.
|
271
|
+
rescue(Exception){|ex| @expected = ex }
|
272
|
+
subject.post{ raise StandardError }
|
273
|
+
sleep(0.1)
|
274
|
+
@expected.should be_a(StandardError)
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'supresses the exception if no rescue matches' do
|
278
|
+
lambda {
|
279
|
+
subject.
|
280
|
+
rescue(ArgumentError){|ex| @expected = ex }.
|
281
|
+
rescue(StandardError){|ex| @expected = ex }.
|
282
|
+
rescue(Exception){|ex| @expected = ex }
|
283
|
+
subject.post{ raise StandardError }
|
284
|
+
sleep(0.1)
|
285
|
+
}.should_not raise_error
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'supresses exceptions thrown from rescue handlers' do
|
289
|
+
lambda {
|
290
|
+
subject.rescue(Exception){ raise StandardError }
|
291
|
+
subject.post{ raise ArgumentError }
|
292
|
+
sleep(0.1)
|
293
|
+
}.should_not raise_error
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context 'observation' do
|
298
|
+
|
299
|
+
it 'notifies all observers when the value changes' do
|
300
|
+
agent = Agent.new(0)
|
301
|
+
agent.add_observer(observer)
|
302
|
+
agent.post{ 10 }
|
303
|
+
sleep(0.1)
|
304
|
+
observer.value.should eq 10
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'does not notify observers when validation fails' do
|
308
|
+
agent = Agent.new(0)
|
309
|
+
agent.validate{ false }
|
310
|
+
agent.add_observer(observer)
|
311
|
+
agent.post{ 10 }
|
312
|
+
sleep(0.1)
|
313
|
+
observer.value.should be_nil
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'does not notify observers when the handler raises an exception' do
|
317
|
+
agent = Agent.new(0)
|
318
|
+
agent.add_observer(observer)
|
319
|
+
agent.post{ raise StandardError }
|
320
|
+
sleep(0.1)
|
321
|
+
observer.value.should be_nil
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
context 'aliases' do
|
326
|
+
|
327
|
+
it 'aliases #deref for #value' do
|
328
|
+
Agent.new(10).deref.should eq 10
|
329
|
+
end
|
330
|
+
|
331
|
+
it 'aliases #validates for :validate' do
|
332
|
+
@expected = nil
|
333
|
+
subject.validates{|v| @expected = v }
|
334
|
+
subject.post{ 10 }
|
335
|
+
sleep(0.1)
|
336
|
+
@expected.should eq 10
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'aliases #validate_with for :validate' do
|
340
|
+
@expected = nil
|
341
|
+
subject.validate_with{|v| @expected = v }
|
342
|
+
subject.post{ 10 }
|
343
|
+
sleep(0.1)
|
344
|
+
@expected.should eq 10
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'aliases #validates_with for :validate' do
|
348
|
+
@expected = nil
|
349
|
+
subject.validates_with{|v| @expected = v }
|
350
|
+
subject.post{ 10 }
|
351
|
+
sleep(0.1)
|
352
|
+
@expected.should eq 10
|
353
|
+
end
|
354
|
+
|
355
|
+
it 'aliases #catch for #rescue' do
|
356
|
+
@expected = nil
|
357
|
+
subject.catch{ @expected = true }
|
358
|
+
subject.post{ raise StandardError }
|
359
|
+
sleep(0.1)
|
360
|
+
@expected.should be_true
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'aliases #on_error for #rescue' do
|
364
|
+
@expected = nil
|
365
|
+
subject.on_error{ @expected = true }
|
366
|
+
subject.post{ raise StandardError }
|
367
|
+
sleep(0.1)
|
368
|
+
@expected.should be_true
|
369
|
+
end
|
370
|
+
|
371
|
+
it 'aliases #add_watch for #add_observer' do
|
372
|
+
agent = Agent.new(0)
|
373
|
+
agent.add_watch(observer)
|
374
|
+
agent.post{ 10 }
|
375
|
+
sleep(0.1)
|
376
|
+
observer.value.should eq 10
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'aliases #<< for Agent#post' do
|
380
|
+
subject << proc{ 100 }
|
381
|
+
sleep(0.1)
|
382
|
+
subject.value.should eq 100
|
383
|
+
|
384
|
+
subject << lambda{ 100 }
|
385
|
+
sleep(0.1)
|
386
|
+
subject.value.should eq 100
|
387
|
+
end
|
388
|
+
|
389
|
+
it 'aliases Kernel#agent for Agent.new' do
|
390
|
+
agent(10).should be_a(Agent)
|
391
|
+
end
|
392
|
+
|
393
|
+
it 'aliases Kernel#deref for #deref' do
|
394
|
+
deref(Agent.new(10)).should eq 10
|
395
|
+
deref(Agent.new(10), 10).should eq 10
|
396
|
+
end
|
397
|
+
|
398
|
+
it 'aliases Kernel:post for Agent#post' do
|
399
|
+
post(subject){ 100 }
|
400
|
+
sleep(0.1)
|
401
|
+
subject.value.should eq 100
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative 'thread_pool_shared'
|
3
|
+
|
4
|
+
module Concurrent
|
5
|
+
|
6
|
+
describe CachedThreadPool do
|
7
|
+
|
8
|
+
subject { CachedThreadPool.new }
|
9
|
+
|
10
|
+
it_should_behave_like 'Thread Pool'
|
11
|
+
|
12
|
+
context '#initialize' do
|
13
|
+
it 'aliases Concurrent#new_cached_thread_pool' do
|
14
|
+
pool = Concurrent.new_cached_thread_pool
|
15
|
+
pool.should be_a(CachedThreadPool)
|
16
|
+
pool.size.should eq 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context '#kill' do
|
21
|
+
|
22
|
+
it 'kills all threads' do
|
23
|
+
Thread.should_receive(:kill).exactly(5).times
|
24
|
+
pool = CachedThreadPool.new
|
25
|
+
5.times{ sleep(0.1); pool << proc{ sleep(1) } }
|
26
|
+
sleep(1)
|
27
|
+
pool.kill
|
28
|
+
sleep(0.1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context '#size' do
|
33
|
+
|
34
|
+
it 'returns zero for a new thread pool' do
|
35
|
+
subject.size.should eq 0
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'returns the size of the subject when running' do
|
39
|
+
5.times{ sleep(0.1); subject << proc{ sleep(1) } }
|
40
|
+
subject.size.should eq 5
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'returns zero once shut down' do
|
44
|
+
subject.shutdown
|
45
|
+
subject.size.should eq 0
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'worker creation and caching' do
|
50
|
+
|
51
|
+
it 'creates new workers when there are none available' do
|
52
|
+
subject.size.should eq 0
|
53
|
+
5.times{ sleep(0.1); subject << proc{ sleep(1000) } }
|
54
|
+
sleep(1)
|
55
|
+
subject.size.should eq 5
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'uses existing idle threads' do
|
59
|
+
5.times{ sleep(0.05); subject << proc{ sleep(0.5) } }
|
60
|
+
sleep(1)
|
61
|
+
3.times{ sleep(0.1); subject << proc{ sleep(0.5) } }
|
62
|
+
subject.size.should eq 5
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'garbage collection' do
|
67
|
+
|
68
|
+
subject{ CachedThreadPool.new(gc_interval: 1, thread_idleime: 1) }
|
69
|
+
|
70
|
+
it 'starts when the first thread is added to the pool' do
|
71
|
+
subject.should_receive(:collect_garbage)
|
72
|
+
subject << proc{ nil }
|
73
|
+
sleep(0.1)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'removes from pool any thread that has been idle too long' do
|
77
|
+
subject << proc{ nil }
|
78
|
+
subject.size.should eq 1
|
79
|
+
sleep(1.5)
|
80
|
+
subject.size.should eq 0
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'removed from pool any dead thread' do
|
84
|
+
subject << proc{ raise StandardError }
|
85
|
+
subject.size.should eq 1
|
86
|
+
sleep(1.5)
|
87
|
+
subject.size.should eq 0
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'resets the working count appropriately' do
|
91
|
+
subject << proc{ sleep(1000) }
|
92
|
+
sleep(0.1)
|
93
|
+
subject << proc{ raise StandardError }
|
94
|
+
sleep(0.1)
|
95
|
+
subject << proc{ nil }
|
96
|
+
|
97
|
+
sleep(0.1)
|
98
|
+
subject.working.should eq 2
|
99
|
+
|
100
|
+
sleep(1.5)
|
101
|
+
subject.working.should eq 1
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'stops collection when the pool size becomes zero' do
|
105
|
+
3.times{ sleep(0.1); subject << proc{ sleep(0.5) } }
|
106
|
+
subject.instance_variable_get(:@collector).status.should eq 'sleep'
|
107
|
+
sleep(1.5)
|
108
|
+
subject.instance_variable_get(:@collector).status.should be_false
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|