concurrent-ruby 0.2.2 → 0.3.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -42
  3. data/lib/concurrent.rb +5 -6
  4. data/lib/concurrent/agent.rb +29 -33
  5. data/lib/concurrent/cached_thread_pool.rb +26 -105
  6. data/lib/concurrent/channel.rb +94 -0
  7. data/lib/concurrent/event.rb +8 -17
  8. data/lib/concurrent/executor.rb +68 -72
  9. data/lib/concurrent/fixed_thread_pool.rb +15 -83
  10. data/lib/concurrent/functions.rb +7 -22
  11. data/lib/concurrent/future.rb +29 -9
  12. data/lib/concurrent/null_thread_pool.rb +5 -2
  13. data/lib/concurrent/obligation.rb +6 -16
  14. data/lib/concurrent/promise.rb +9 -10
  15. data/lib/concurrent/runnable.rb +103 -0
  16. data/lib/concurrent/supervisor.rb +271 -44
  17. data/lib/concurrent/thread_pool.rb +112 -39
  18. data/lib/concurrent/version.rb +1 -1
  19. data/md/executor.md +9 -3
  20. data/md/goroutine.md +11 -9
  21. data/md/reactor.md +32 -0
  22. data/md/supervisor.md +43 -0
  23. data/spec/concurrent/agent_spec.rb +128 -51
  24. data/spec/concurrent/cached_thread_pool_spec.rb +33 -47
  25. data/spec/concurrent/channel_spec.rb +446 -0
  26. data/spec/concurrent/event_machine_defer_proxy_spec.rb +3 -1
  27. data/spec/concurrent/event_spec.rb +0 -19
  28. data/spec/concurrent/executor_spec.rb +167 -119
  29. data/spec/concurrent/fixed_thread_pool_spec.rb +40 -30
  30. data/spec/concurrent/functions_spec.rb +0 -20
  31. data/spec/concurrent/future_spec.rb +88 -0
  32. data/spec/concurrent/null_thread_pool_spec.rb +23 -2
  33. data/spec/concurrent/obligation_shared.rb +0 -5
  34. data/spec/concurrent/promise_spec.rb +9 -10
  35. data/spec/concurrent/runnable_shared.rb +62 -0
  36. data/spec/concurrent/runnable_spec.rb +233 -0
  37. data/spec/concurrent/supervisor_spec.rb +912 -47
  38. data/spec/concurrent/thread_pool_shared.rb +18 -31
  39. data/spec/spec_helper.rb +10 -3
  40. metadata +17 -23
  41. data/lib/concurrent/defer.rb +0 -65
  42. data/lib/concurrent/reactor.rb +0 -166
  43. data/lib/concurrent/reactor/drb_async_demux.rb +0 -83
  44. data/lib/concurrent/reactor/tcp_sync_demux.rb +0 -131
  45. data/lib/concurrent/utilities.rb +0 -32
  46. data/md/defer.md +0 -174
  47. data/spec/concurrent/defer_spec.rb +0 -199
  48. data/spec/concurrent/reactor/drb_async_demux_spec.rb +0 -196
  49. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +0 -410
  50. data/spec/concurrent/reactor_spec.rb +0 -364
  51. data/spec/concurrent/utilities_spec.rb +0 -74
@@ -5,28 +5,27 @@ module Concurrent
5
5
 
6
6
  describe CachedThreadPool do
7
7
 
8
- subject { CachedThreadPool.new }
8
+ subject { CachedThreadPool.new(max_threads: 5) }
9
9
 
10
- it_should_behave_like 'Thread Pool'
10
+ after(:each) do
11
+ subject.kill
12
+ sleep(0.1)
13
+ end
14
+
15
+ it_should_behave_like :thread_pool
11
16
 
12
17
  context '#initialize' do
13
18
 
14
- it 'aliases Concurrent#new_cached_thread_pool' do
15
- pool = Concurrent.new_cached_thread_pool
16
- pool.should be_a(CachedThreadPool)
17
- pool.size.should eq 0
19
+ it 'raises an exception when the pool size is less than one' do
20
+ lambda {
21
+ CachedThreadPool.new(max: 0)
22
+ }.should raise_error(ArgumentError)
18
23
  end
19
- end
20
24
 
21
- context '#kill' do
22
-
23
- it 'kills all threads' do
24
- Thread.should_receive(:kill).at_least(5).times
25
- pool = CachedThreadPool.new
26
- 5.times{ sleep(0.1); pool << proc{ sleep(1) } }
27
- sleep(1)
28
- pool.kill
29
- sleep(0.1)
25
+ it 'raises an exception when the pool size is greater than MAX_POOL_SIZE' do
26
+ lambda {
27
+ CachedThreadPool.new(max: CachedThreadPool::MAX_POOL_SIZE + 1)
28
+ }.should raise_error(ArgumentError)
30
29
  end
31
30
  end
32
31
 
@@ -51,28 +50,36 @@ module Concurrent
51
50
 
52
51
  it 'creates new workers when there are none available' do
53
52
  subject.size.should eq 0
54
- 5.times{ sleep(0.1); subject << proc{ sleep(1000) } }
53
+ 5.times{ sleep(0.1); subject << proc{ sleep } }
55
54
  sleep(1)
56
55
  subject.size.should eq 5
57
56
  end
58
57
 
59
58
  it 'uses existing idle threads' do
60
- 5.times{ sleep(0.05); subject << proc{ sleep(0.5) } }
59
+ 5.times{ subject << proc{ sleep(0.1) } }
61
60
  sleep(1)
62
- 3.times{ sleep(0.1); subject << proc{ sleep(0.5) } }
63
61
  subject.size.should eq 5
62
+ 3.times{ subject << proc{ sleep } }
63
+ sleep(0.1)
64
+ subject.size.should eq 5
65
+ end
66
+
67
+ it 'never creates more than :max_threads threads' do
68
+ pool = CachedThreadPool.new(max: 5)
69
+ 100.times{ sleep(0.01); pool << proc{ sleep } }
70
+ sleep(0.1)
71
+ pool.length.should eq 5
72
+ pool.kill
73
+ end
74
+
75
+ it 'sets :max_threads to MAX_POOL_SIZE when not given' do
76
+ CachedThreadPool.new.max_threads.should eq CachedThreadPool::MAX_POOL_SIZE
64
77
  end
65
78
  end
66
79
 
67
80
  context 'garbage collection' do
68
81
 
69
- subject{ CachedThreadPool.new(gc_interval: 1, thread_idleime: 1) }
70
-
71
- it 'starts when the first thread is added to the pool' do
72
- subject.should_receive(:collect_garbage)
73
- subject << proc{ nil }
74
- sleep(0.1)
75
- end
82
+ subject{ CachedThreadPool.new(gc_interval: 1, idletime: 0.1) }
76
83
 
77
84
  it 'removes from pool any thread that has been idle too long' do
78
85
  subject << proc{ nil }
@@ -87,27 +94,6 @@ module Concurrent
87
94
  sleep(1.5)
88
95
  subject.size.should eq 0
89
96
  end
90
-
91
- it 'resets the working count appropriately' do
92
- subject << proc{ sleep(1000) }
93
- sleep(0.1)
94
- subject << proc{ raise StandardError }
95
- sleep(0.1)
96
- subject << proc{ nil }
97
-
98
- sleep(0.1)
99
- subject.working.should eq 2
100
-
101
- sleep(1.5)
102
- subject.working.should eq 1
103
- end
104
-
105
- it 'stops collection when the pool size becomes zero' do
106
- 3.times{ sleep(0.1); subject << proc{ sleep(0.5) } }
107
- subject.instance_variable_get(:@collector).status.should eq 'sleep'
108
- sleep(1.5)
109
- subject.instance_variable_get(:@collector).status.should be_false
110
- end
111
97
  end
112
98
 
113
99
  context '#status' do
@@ -0,0 +1,446 @@
1
+ require 'spec_helper'
2
+ require_relative 'runnable_shared'
3
+
4
+ module Concurrent
5
+
6
+ describe Channel do
7
+
8
+ subject { Channel.new }
9
+ let(:runnable) { Channel }
10
+
11
+ it_should_behave_like :runnable
12
+
13
+ after(:each) do
14
+ subject.stop
15
+ @thread.kill unless @thread.nil?
16
+ sleep(0.1)
17
+ end
18
+
19
+ context '#post' do
20
+
21
+ it 'returns false when not running' do
22
+ subject.post.should be_false
23
+ end
24
+
25
+ it 'pushes a message onto the queue' do
26
+ @expected = false
27
+ channel = Channel.new{|msg| @expected = msg }
28
+ @thread = Thread.new{ channel.run }
29
+ @thread.join(0.1)
30
+ channel.post(true)
31
+ @thread.join(0.1)
32
+ @expected.should be_true
33
+ channel.stop
34
+ end
35
+
36
+ it 'returns the current size of the queue' do
37
+ channel = Channel.new{|msg| sleep }
38
+ @thread = Thread.new{ channel.run }
39
+ @thread.join(0.1)
40
+ channel.post(true).should == 1
41
+ @thread.join(0.1)
42
+ channel.post(true).should == 1
43
+ @thread.join(0.1)
44
+ channel.post(true).should == 2
45
+ channel.stop
46
+ end
47
+
48
+ it 'is aliased a <<' do
49
+ @expected = false
50
+ channel = Channel.new{|msg| @expected = msg }
51
+ @thread = Thread.new{ channel.run }
52
+ @thread.join(0.1)
53
+ channel << true
54
+ @thread.join(0.1)
55
+ @expected.should be_true
56
+ channel.stop
57
+ end
58
+ end
59
+
60
+ context '#run' do
61
+
62
+ it 'empties the queue' do
63
+ @thread = Thread.new{ subject.run }
64
+ @thread.join(0.1)
65
+ q = subject.instance_variable_get(:@queue)
66
+ q.size.should == 0
67
+ end
68
+ end
69
+
70
+ context '#stop' do
71
+
72
+ it 'empties the queue' do
73
+ channel = Channel.new{|msg| sleep }
74
+ @thread = Thread.new{ channel.run }
75
+ 10.times { channel.post(true) }
76
+ @thread.join(0.1)
77
+ channel.stop
78
+ @thread.join(0.1)
79
+ q = channel.instance_variable_get(:@queue)
80
+ if q.size >= 1
81
+ q.pop.should == :stop
82
+ else
83
+ q.size.should == 0
84
+ end
85
+ end
86
+
87
+ it 'pushes a :stop message onto the queue' do
88
+ @thread = Thread.new{ subject.run }
89
+ @thread.join(0.1)
90
+ q = subject.instance_variable_get(:@queue)
91
+ q.should_receive(:push).once.with(:stop)
92
+ subject.stop
93
+ @thread.join(0.1)
94
+ end
95
+ end
96
+
97
+ context 'message handling' do
98
+
99
+ it 'runs the constructor block once for every message' do
100
+ @expected = 0
101
+ channel = Channel.new{|msg| @expected += 1 }
102
+ @thread = Thread.new{ channel.run }
103
+ @thread.join(0.1)
104
+ 10.times { channel.post(true) }
105
+ @thread.join(0.1)
106
+ @expected.should eq 10
107
+ channel.stop
108
+ end
109
+
110
+ it 'passes the message to the block' do
111
+ @expected = []
112
+ channel = Channel.new{|msg| @expected << msg }
113
+ @thread = Thread.new{ channel.run }
114
+ @thread.join(0.1)
115
+ 10.times {|i| channel.post(i) }
116
+ @thread.join(0.1)
117
+ channel.stop
118
+ @expected.should eq (0..9).to_a
119
+ end
120
+ end
121
+
122
+ context 'exception handling' do
123
+
124
+ it 'supresses exceptions thrown when handling messages' do
125
+ channel = Channel.new{|msg| raise StandardError }
126
+ @thread = Thread.new{ channel.run }
127
+ expect {
128
+ @thread.join(0.1)
129
+ 10.times { channel.post(true) }
130
+ }.not_to raise_error
131
+ channel.stop
132
+ end
133
+
134
+ it 'calls the errorback with the time, message, and exception' do
135
+ @expected = []
136
+ errorback = proc{|*args| @expected = args }
137
+ channel = Channel.new(errorback){|msg| raise StandardError }
138
+ @thread = Thread.new{ channel.run }
139
+ @thread.join(0.1)
140
+ channel.post(42)
141
+ @thread.join(0.1)
142
+ @expected[0].should be_a(Time)
143
+ @expected[1].should == [42]
144
+ @expected[2].should be_a(StandardError)
145
+ channel.stop
146
+ end
147
+ end
148
+
149
+ context 'observer notification' do
150
+
151
+ let(:observer) do
152
+ Class.new {
153
+ attr_reader :notice
154
+ def update(*args) @notice = args; end
155
+ }.new
156
+ end
157
+
158
+ it 'notifies observers when a message is successfully handled' do
159
+ observer.should_receive(:update).exactly(10).times.with(any_args())
160
+ subject.add_observer(observer)
161
+ @thread = Thread.new{ subject.run }
162
+ @thread.join(0.1)
163
+ 10.times { subject.post(true) }
164
+ @thread.join(0.1)
165
+ end
166
+
167
+ it 'does not notify observers when a message raises an exception' do
168
+ observer.should_not_receive(:update).with(any_args())
169
+ channel = Channel.new{|msg| raise StandardError }
170
+ channel.add_observer(observer)
171
+ @thread = Thread.new{ channel.run }
172
+ @thread.join(0.1)
173
+ 10.times { channel.post(true) }
174
+ @thread.join(0.1)
175
+ channel.stop
176
+ end
177
+
178
+ it 'passes the time, message, and result to the observer' do
179
+ channel = Channel.new{|*msg| msg }
180
+ channel.add_observer(observer)
181
+ @thread = Thread.new{ channel.run }
182
+ @thread.join(0.1)
183
+ channel.post(42)
184
+ @thread.join(0.1)
185
+ observer.notice[0].should be_a(Time)
186
+ observer.notice[1].should == [42]
187
+ observer.notice[2].should == [42]
188
+ channel.stop
189
+ end
190
+ end
191
+
192
+ context '#pool' do
193
+
194
+ let(:clazz){ Class.new(Channel) }
195
+
196
+ it 'raises an exception if the count is zero or less' do
197
+ expect {
198
+ clazz.pool(0)
199
+ }.to raise_error(ArgumentError)
200
+ end
201
+
202
+ it 'creates the requested number of channels' do
203
+ mailbox, channels = clazz.pool(5)
204
+ channels.size.should == 5
205
+ end
206
+
207
+ it 'passes the errorback to each channel' do
208
+ errorback = proc{ nil }
209
+ clazz.should_receive(:new).with(errorback)
210
+ clazz.pool(1, errorback)
211
+ end
212
+
213
+ it 'passes the block to each channel' do
214
+ block = proc{ nil }
215
+ clazz.should_receive(:new).with(anything(), &block)
216
+ clazz.pool(1, nil, &block)
217
+ end
218
+
219
+ it 'gives all channels the same mailbox' do
220
+ mailbox, channels = clazz.pool(2)
221
+ mbox1 = channels.first.instance_variable_get(:@queue)
222
+ mbox2 = channels.last.instance_variable_get(:@queue)
223
+ mbox1.should eq mbox2
224
+ end
225
+
226
+ it 'returns a Poolbox as the first retval' do
227
+ mailbox, channels = clazz.pool(2)
228
+ mailbox.should be_a(Channel::Poolbox)
229
+ end
230
+
231
+ it 'gives the Poolbox the same mailbox as the channels' do
232
+ mailbox, channels = clazz.pool(1)
233
+ mbox1 = mailbox.instance_variable_get(:@queue)
234
+ mbox2 = channels.first.instance_variable_get(:@queue)
235
+ mbox1.should eq mbox2
236
+ end
237
+
238
+ it 'returns an array of channels as the second retval' do
239
+ mailbox, channels = clazz.pool(2)
240
+ channels.each do |channel|
241
+ channel.should be_a(clazz)
242
+ end
243
+ end
244
+
245
+ it 'posts to the mailbox with Poolbox#post' do
246
+ @expected = false
247
+ mailbox, channels = clazz.pool(1){|msg| @expected = true }
248
+ @thread = Thread.new{ channels.first.run }
249
+ sleep(0.1)
250
+ mailbox.post(42)
251
+ sleep(0.1)
252
+ channels.each{|channel| channel.stop }
253
+ @thread.kill
254
+ @expected.should be_true
255
+ end
256
+
257
+ it 'posts to the mailbox with Poolbox#<<' do
258
+ @expected = false
259
+ mailbox, channels = clazz.pool(1){|msg| @expected = true }
260
+ @thread = Thread.new{ channels.first.run }
261
+ sleep(0.1)
262
+ mailbox << 42
263
+ sleep(0.1)
264
+ channels.each{|channel| channel.stop }
265
+ @thread.kill
266
+ @expected.should be_true
267
+ end
268
+ end
269
+
270
+ context 'subclassing' do
271
+
272
+ after(:each) do
273
+ @thread.kill unless @thread.nil?
274
+ end
275
+
276
+ context '#pool' do
277
+
278
+ it 'creates channels of the appropriate subclass' do
279
+ actor = Class.new(Channel)
280
+ mailbox, channels = actor.pool(1)
281
+ channels.first.should be_a(actor)
282
+ end
283
+ end
284
+
285
+ context '#receive overloading' do
286
+
287
+ let(:actor) do
288
+ Class.new(Channel) {
289
+ attr_reader :last_message
290
+ def receive(*message)
291
+ @last_message = message
292
+ end
293
+ }
294
+ end
295
+
296
+ it 'ignores the constructor block' do
297
+ @expected = false
298
+ channel = actor.new{|*args| @expected = true }
299
+ @thread = Thread.new{ channel.run }
300
+ @thread.join(0.1)
301
+ channel.post(:foo)
302
+ @thread.join(0.1)
303
+ @expected.should be_false
304
+ channel.stop
305
+ end
306
+
307
+ it 'uses the subclass receive implementation' do
308
+ channel = actor.new{|*args| @expected = true }
309
+ @thread = Thread.new{ channel.run }
310
+ @thread.join(0.1)
311
+ channel.post(:foo)
312
+ @thread.join(0.1)
313
+ channel.last_message.should eq [:foo]
314
+ channel.stop
315
+ end
316
+ end
317
+
318
+ context '#receive pattern matching' do
319
+
320
+ let(:actor) do
321
+ Class.new(Channel) {
322
+ include PatternMatching
323
+ attr_reader :last_message
324
+ defn(:receive, :foo){|*args| @last_message = 'FOO' }
325
+ defn(:receive, :foo, :bar){|_, _| @last_message = 'FUBAR'}
326
+ }
327
+ end
328
+
329
+ it 'recognizes #defn pattern matches' do
330
+ channel = actor.new
331
+ @thread = Thread.new{ channel.run }
332
+ @thread.join(0.1)
333
+
334
+ channel.post(:foo)
335
+ @thread.join(0.1)
336
+ channel.last_message.should eq 'FOO'
337
+
338
+ channel.post(:foo, :bar)
339
+ @thread.join(0.1)
340
+ channel.last_message.should eq 'FUBAR'
341
+
342
+ channel.stop
343
+ end
344
+
345
+ it 'falls back to the superclass #receive on no match' do
346
+ @expected = false
347
+ channel = actor.new{|*args| @expected = true }
348
+ @thread = Thread.new{ channel.run }
349
+ @thread.join(0.1)
350
+
351
+ channel.post(1, 2, 3, 4, 5)
352
+ @thread.join(0.1)
353
+ @expected.should be_true
354
+
355
+ channel.stop
356
+ end
357
+ end
358
+
359
+ context '#on_error overloading' do
360
+
361
+ let(:actor) do
362
+ Class.new(Channel) {
363
+ attr_reader :last_error
364
+ def receive(*message)
365
+ raise StandardError
366
+ end
367
+ def on_error(*args)
368
+ @last_error = args
369
+ end
370
+ }
371
+ end
372
+
373
+ it 'ignores the constructor errorback' do
374
+ @expected = false
375
+ errorback = proc{|*args| @expected = true }
376
+ channel = actor.new(errorback)
377
+ @thread = Thread.new{ channel.run }
378
+ @thread.join(0.1)
379
+ channel.post(true)
380
+ @thread.join(0.1)
381
+ @expected.should be_false
382
+ channel.stop
383
+ end
384
+
385
+ it 'uses the subclass #on_error implementation' do
386
+ channel = actor.new
387
+ @thread = Thread.new{ channel.run }
388
+ @thread.join(0.1)
389
+ channel.post(42)
390
+ @thread.join(0.1)
391
+ channel.last_error[0].should be_a(Time)
392
+ channel.last_error[1].should eq [42]
393
+ channel.last_error[2].should be_a(StandardError)
394
+ channel.stop
395
+ end
396
+ end
397
+ end
398
+
399
+ context 'supervision' do
400
+
401
+ it 'can be started by a Supervisor' do
402
+ channel = Channel.new
403
+ supervisor = Supervisor.new
404
+ supervisor.add_worker(channel)
405
+
406
+ channel.should_receive(:run).with(no_args())
407
+ supervisor.run!
408
+ sleep(0.1)
409
+
410
+ supervisor.stop
411
+ sleep(0.1)
412
+ channel.stop
413
+ end
414
+
415
+ it 'can receive messages while under supervision' do
416
+ @expected = false
417
+ channel = Channel.new{|*args| @expected = true}
418
+ supervisor = Supervisor.new
419
+ supervisor.add_worker(channel)
420
+ supervisor.run!
421
+ sleep(0.1)
422
+
423
+ channel.post(42)
424
+ sleep(0.1)
425
+ @expected.should be_true
426
+
427
+ supervisor.stop
428
+ sleep(0.1)
429
+ channel.stop
430
+ end
431
+
432
+ it 'can be stopped by a supervisor' do
433
+ channel = Channel.new
434
+ supervisor = Supervisor.new
435
+ supervisor.add_worker(channel)
436
+
437
+ supervisor.run!
438
+ sleep(0.1)
439
+
440
+ channel.should_receive(:stop).with(no_args())
441
+ supervisor.stop
442
+ sleep(0.1)
443
+ end
444
+ end
445
+ end
446
+ end