concurrent-ruby 0.2.2 → 0.3.0.pre.1

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 (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