concurrent-ruby 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -1
  3. data/lib/concurrent.rb +8 -1
  4. data/lib/concurrent/agent.rb +19 -40
  5. data/lib/concurrent/cached_thread_pool.rb +10 -11
  6. data/lib/concurrent/defer.rb +8 -12
  7. data/lib/concurrent/executor.rb +95 -0
  8. data/lib/concurrent/fixed_thread_pool.rb +12 -6
  9. data/lib/concurrent/functions.rb +120 -0
  10. data/lib/concurrent/future.rb +8 -20
  11. data/lib/concurrent/global_thread_pool.rb +13 -0
  12. data/lib/concurrent/goroutine.rb +5 -1
  13. data/lib/concurrent/null_thread_pool.rb +22 -0
  14. data/lib/concurrent/obligation.rb +10 -64
  15. data/lib/concurrent/promise.rb +38 -60
  16. data/lib/concurrent/reactor.rb +166 -0
  17. data/lib/concurrent/reactor/drb_async_demux.rb +83 -0
  18. data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -0
  19. data/lib/concurrent/supervisor.rb +100 -0
  20. data/lib/concurrent/thread_pool.rb +16 -5
  21. data/lib/concurrent/utilities.rb +8 -0
  22. data/lib/concurrent/version.rb +1 -1
  23. data/md/defer.md +4 -4
  24. data/md/executor.md +187 -0
  25. data/md/promise.md +2 -0
  26. data/md/thread_pool.md +27 -0
  27. data/spec/concurrent/agent_spec.rb +8 -27
  28. data/spec/concurrent/cached_thread_pool_spec.rb +14 -1
  29. data/spec/concurrent/defer_spec.rb +17 -21
  30. data/spec/concurrent/event_machine_defer_proxy_spec.rb +159 -149
  31. data/spec/concurrent/executor_spec.rb +200 -0
  32. data/spec/concurrent/fixed_thread_pool_spec.rb +2 -3
  33. data/spec/concurrent/functions_spec.rb +217 -0
  34. data/spec/concurrent/future_spec.rb +4 -11
  35. data/spec/concurrent/global_thread_pool_spec.rb +38 -0
  36. data/spec/concurrent/goroutine_spec.rb +15 -0
  37. data/spec/concurrent/null_thread_pool_spec.rb +54 -0
  38. data/spec/concurrent/obligation_shared.rb +127 -116
  39. data/spec/concurrent/promise_spec.rb +16 -14
  40. data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -0
  41. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -0
  42. data/spec/concurrent/reactor_spec.rb +364 -0
  43. data/spec/concurrent/supervisor_spec.rb +258 -0
  44. data/spec/concurrent/thread_pool_shared.rb +156 -161
  45. data/spec/concurrent/utilities_spec.rb +30 -1
  46. data/spec/spec_helper.rb +13 -0
  47. metadata +38 -9
@@ -0,0 +1,364 @@
1
+ require 'spec_helper'
2
+
3
+ module Concurrent
4
+
5
+ describe Reactor do
6
+
7
+ let(:sync_demux) do
8
+ Class.new {
9
+ def initialize
10
+ @running = false
11
+ @queue = Queue.new
12
+ end
13
+ def run() @running = true; end
14
+ def stop
15
+ @queue.push(:stop)
16
+ @running = false
17
+ end
18
+ def running?() return @running == true; end
19
+ def accept()
20
+ event = @queue.pop
21
+ if event == :stop
22
+ return nil
23
+ else
24
+ return Reactor::EventContext.new(event)
25
+ end
26
+ end
27
+ def respond(result, message) return [result, message]; end
28
+ def send(event) @queue.push(event) end
29
+ }.new
30
+ end
31
+
32
+ let(:async_demux) do
33
+ Class.new {
34
+ def initialize() @running = false; end
35
+ def run() @running = true; end
36
+ def stop() @running = false; end
37
+ def running?() return @running == true; end
38
+ def set_reactor(reactor) @reactor = reactor; end
39
+ def send(event) @reactor.handle(event); end
40
+ }.new
41
+ end
42
+
43
+ after(:each) do
44
+ Thread.kill(@thread) unless @thread.nil?
45
+ end
46
+
47
+ context '#initialize' do
48
+
49
+ it 'raises an exception when the demux is not valid' do
50
+ lambda {
51
+ Reactor.new('bogus demux')
52
+ }.should raise_error(ArgumentError)
53
+ end
54
+
55
+ it 'sets the initial state to not running' do
56
+ Reactor.new.should_not be_running
57
+ end
58
+ end
59
+
60
+ context '#running?' do
61
+
62
+ it 'returns true when the reactor is running' do
63
+ reactor = Reactor.new
64
+ @thread = Thread.new{ reactor.run }
65
+ sleep(0.1)
66
+ reactor.should be_running
67
+ reactor.stop
68
+ end
69
+
70
+ it 'returns false when the reactor is stopped' do
71
+ reactor = Reactor.new
72
+ @thread = Thread.new{ reactor.run }
73
+ sleep(0.1)
74
+ reactor.stop
75
+ sleep(0.1)
76
+ reactor.should_not be_running
77
+ end
78
+ end
79
+
80
+ context '#add_handler' do
81
+
82
+ it 'raises an exception is the event name is reserved' do
83
+ reactor = Reactor.new
84
+ lambda {
85
+ reactor.add_handler(Reactor::RESERVED_EVENTS.first){ nil }
86
+ }.should raise_error(ArgumentError)
87
+ end
88
+
89
+ it 'raises an exception if no block is given' do
90
+ reactor = Reactor.new
91
+ lambda {
92
+ reactor.add_handler('no block given')
93
+ }.should raise_error(ArgumentError)
94
+ end
95
+
96
+ it 'returns true if the handler is added' do
97
+ reactor = Reactor.new
98
+ reactor.add_handler('good'){ nil }.should be_true
99
+ end
100
+ end
101
+
102
+ context '#remove_handler' do
103
+
104
+ it 'returns true if the handler is found and removed' do
105
+ reactor = Reactor.new
106
+ reactor.add_handler('good'){ nil }
107
+ reactor.remove_handler('good').should be_true
108
+ end
109
+
110
+ it 'returns false if the handler is not found' do
111
+ reactor = Reactor.new
112
+ reactor.remove_handler('not found').should be_false
113
+ end
114
+ end
115
+
116
+ context '#stop_on_signal' do
117
+
118
+ if Functional::PLATFORM.mri? && ! Functional::PLATFORM.windows?
119
+
120
+ it 'traps each valid signal' do
121
+ Signal.should_receive(:trap).with('USR1')
122
+ Signal.should_receive(:trap).with('USR2')
123
+ reactor = Reactor.new
124
+ reactor.stop_on_signal('USR1', 'USR2')
125
+ end
126
+
127
+ it 'raises an exception if given an invalid signal' do
128
+ if Functional::PLATFORM.mri?
129
+ reactor = Reactor.new
130
+ lambda {
131
+ reactor.stop_on_signal('BOGUS')
132
+ }.should raise_error(ArgumentError)
133
+ end
134
+ end
135
+
136
+ it 'stops the reactor when it receives a trapped signal' do
137
+ reactor = Reactor.new
138
+ reactor.stop_on_signal('USR1')
139
+ reactor.should_receive(:stop).with(no_args())
140
+ Process.kill('USR1', Process.pid)
141
+ sleep(0.1)
142
+ end
143
+ end
144
+ end
145
+
146
+ context '#handle' do
147
+
148
+ it 'raises an exception if the demux is synchronous' do
149
+ reactor = Reactor.new(sync_demux)
150
+ lambda {
151
+ reactor.handle('event')
152
+ }.should raise_error(NotImplementedError)
153
+ end
154
+
155
+ it 'returns :stopped if the reactor is not running' do
156
+ reactor = Reactor.new
157
+ reactor.handle('event').first.should eq :stopped
158
+ end
159
+
160
+ it 'returns :ok and the block result on success' do
161
+ reactor = Reactor.new
162
+ reactor.add_handler(:event){ 10 }
163
+ @thread = Thread.new{ reactor.run }
164
+ sleep(0.1)
165
+ result = reactor.handle(:event)
166
+ result.first.should eq :ok
167
+ result.last.should eq 10
168
+ reactor.stop
169
+ end
170
+
171
+ it 'returns :ex and the exception on failure' do
172
+ reactor = Reactor.new
173
+ reactor.add_handler(:event){ raise StandardError }
174
+ @thread = Thread.new{ reactor.run }
175
+ sleep(0.1)
176
+ result = reactor.handle(:event)
177
+ result.first.should eq :ex
178
+ result.last.should be_a(StandardError)
179
+ reactor.stop
180
+ end
181
+
182
+ it 'returns :noop when there is no handler' do
183
+ reactor = Reactor.new
184
+ @thread = Thread.new{ reactor.run }
185
+ sleep(0.1)
186
+ result = reactor.handle(:event)
187
+ sleep(0.1)
188
+ result.first.should eq :noop
189
+ reactor.stop
190
+ end
191
+
192
+ it 'triggers handlers added after the reactor is runed' do
193
+ @expected = false
194
+ reactor = Reactor.new
195
+ @thread = Thread.new{ reactor.run }
196
+ sleep(0.1)
197
+ reactor.add_handler(:event){ @expected = true }
198
+ reactor.handle(:event)
199
+ @expected.should be_true
200
+ reactor.stop
201
+ end
202
+
203
+ it 'does not trigger an event that was removed' do
204
+ @expected = false
205
+ reactor = Reactor.new
206
+ reactor.add_handler(:event){ @expected = true }
207
+ reactor.remove_handler(:event)
208
+ @thread = Thread.new{ reactor.run }
209
+ sleep(0.1)
210
+ reactor.handle(:event)
211
+ @expected.should be_false
212
+ reactor.stop
213
+ end
214
+ end
215
+
216
+ context '#run' do
217
+
218
+ it 'raises an exception if the reactor is already running' do
219
+ reactor = Reactor.new
220
+ @thread = Thread.new{ reactor.run }
221
+ sleep(0.1)
222
+ lambda {
223
+ reactor.run
224
+ }.should raise_error(StandardError)
225
+ reactor.stop
226
+ end
227
+
228
+ it 'runs the reactor if it is not running' do
229
+ reactor = Reactor.new(async_demux)
230
+ reactor.should_receive(:run_async).with(no_args())
231
+ @thread = Thread.new{ reactor.run }
232
+ sleep(0.1)
233
+ reactor.should be_running
234
+ reactor.stop
235
+
236
+ reactor = Reactor.new(sync_demux)
237
+ reactor.should_receive(:run_sync).with(no_args())
238
+ @thread = Thread.new{ reactor.run }
239
+ sleep(0.1)
240
+ reactor.should be_running
241
+ reactor.stop
242
+ end
243
+ end
244
+
245
+ context '#stop' do
246
+
247
+ it 'returns if the reactor is not running' do
248
+ reactor = Reactor.new
249
+ reactor.stop.should be_true
250
+ end
251
+
252
+ it 'stops the reactor when running and synchronous' do
253
+ reactor = Reactor.new(sync_demux)
254
+ @thread = Thread.new{ sleep(0.1); reactor.stop }
255
+ Thread.pass
256
+ reactor.run
257
+ end
258
+
259
+ it 'stops the reactor when running and asynchronous' do
260
+ reactor = Reactor.new(async_demux)
261
+ @thread = Thread.new{ sleep(0.1); reactor.stop }
262
+ Thread.pass
263
+ reactor.run
264
+ end
265
+
266
+ it 'stops the reactor when running without a demux' do
267
+ reactor = Reactor.new
268
+ @thread = Thread.new{ sleep(0.1); reactor.stop }
269
+ Thread.pass
270
+ reactor.run
271
+ end
272
+ end
273
+
274
+ specify 'synchronous demultiplexing' do
275
+
276
+ if Functional::PLATFORM.mri? && ! Functional::PLATFORM.windows?
277
+
278
+ demux = sync_demux
279
+ reactor = Concurrent::Reactor.new(demux)
280
+
281
+ reactor.should_not be_running
282
+
283
+ reactor.add_handler(:foo){ 'Foo' }
284
+ reactor.add_handler(:bar){ 'Bar' }
285
+ reactor.add_handler(:baz){ 'Baz' }
286
+ reactor.add_handler(:fubar){ raise StandardError.new('Boom!') }
287
+
288
+ reactor.stop_on_signal('USR1')
289
+
290
+ demux.should_receive(:respond).with(:ok, 'Foo')
291
+ demux.send(:foo)
292
+
293
+ @thread = Thread.new do
294
+ reactor.run
295
+ end
296
+ @thread.abort_on_exception = true
297
+ sleep(0.1)
298
+
299
+ reactor.should be_running
300
+
301
+ demux.should_receive(:respond).with(:ok, 'Bar')
302
+ demux.should_receive(:respond).with(:ok, 'Baz')
303
+ demux.should_receive(:respond).with(:noop, anything())
304
+ demux.should_receive(:respond).with(:ex, anything())
305
+
306
+ demux.send(:bar)
307
+ demux.send(:baz)
308
+ demux.send(:bogus)
309
+ demux.send(:fubar)
310
+
311
+ reactor.should be_running
312
+
313
+ Process.kill('USR1', Process.pid)
314
+ sleep(0.1)
315
+
316
+ demux.should_not_receive(:respond).with(:foo, anything())
317
+ demux.send(:foo)
318
+ reactor.should_not be_running
319
+ end
320
+ end
321
+
322
+ specify 'asynchronous demultiplexing' do
323
+
324
+ if Functional::PLATFORM.mri? && ! Functional::PLATFORM.windows?
325
+
326
+ demux = async_demux
327
+ reactor = Concurrent::Reactor.new(demux)
328
+
329
+ reactor.should_not be_running
330
+
331
+ reactor.add_handler(:foo){ 'Foo' }
332
+ reactor.add_handler(:bar){ 'Bar' }
333
+ reactor.add_handler(:baz){ 'Baz' }
334
+ reactor.add_handler(:fubar){ raise StandardError.new('Boom!') }
335
+
336
+ reactor.stop_on_signal('USR2')
337
+
338
+ demux.send(:foo).first.should eq :stopped
339
+
340
+ @thread = Thread.new do
341
+ reactor.run
342
+ end
343
+ @thread.abort_on_exception = true
344
+ sleep(0.1)
345
+
346
+ reactor.should be_running
347
+
348
+ demux.send(:foo).should eq [:ok, 'Foo']
349
+ demux.send(:bar).should eq [:ok, 'Bar']
350
+ demux.send(:baz).should eq [:ok, 'Baz']
351
+ demux.send(:bogus).first.should eq :noop
352
+ demux.send(:fubar).first.should eq :ex
353
+
354
+ reactor.should be_running
355
+
356
+ Process.kill('USR2', Process.pid)
357
+ sleep(0.1)
358
+
359
+ demux.send(:foo).first.should eq :stopped
360
+ reactor.should_not be_running
361
+ end
362
+ end
363
+ end
364
+ end
@@ -0,0 +1,258 @@
1
+ require 'spec_helper'
2
+
3
+ module Concurrent
4
+
5
+ describe Supervisor do
6
+
7
+ let(:worker_class) do
8
+ Class.new {
9
+ behavior(:runnable)
10
+ def run() return true; end
11
+ def stop() return true; end
12
+ def running?() return true; end
13
+ }
14
+ end
15
+
16
+ let(:worker){ worker_class.new }
17
+
18
+ subject{ Supervisor.new }
19
+
20
+ after(:each) do
21
+ subject.stop
22
+ end
23
+
24
+ context '#initialize' do
25
+
26
+ it 'sets the initial length to zero' do
27
+ supervisor = Supervisor.new
28
+ supervisor.length.should == 0
29
+ end
30
+
31
+ it 'sets the initial length to one when a worker is provided' do
32
+ supervisor = Supervisor.new(worker: worker)
33
+ supervisor.length.should == 1
34
+ end
35
+
36
+ it 'sets the initial state to stopped' do
37
+ supervisor = Supervisor.new
38
+ supervisor.should_not be_running
39
+ end
40
+
41
+ it 'sets the monitor interval when given' do
42
+ supervisor = Supervisor.new
43
+ supervisor.monitor_interval.should == Supervisor::DEFAULT_MONITOR_INTERVAL
44
+ end
45
+
46
+ it 'sets the monitor interval to the default when not given' do
47
+ supervisor = Supervisor.new(monitor_interval: 5)
48
+ supervisor.monitor_interval.should == 5
49
+
50
+ supervisor = Supervisor.new(monitor: 10)
51
+ supervisor.monitor_interval.should == 10
52
+ end
53
+ end
54
+
55
+ context 'run' do
56
+
57
+ it 'runs the monitor' do
58
+ subject.should_receive(:monitor).with(no_args()).at_least(1).times
59
+ t = Thread.new{ subject.run }
60
+ sleep(0.1)
61
+ subject.stop
62
+ Thread.kill(t) unless t.nil?
63
+ end
64
+
65
+ it 'calls #run on all workers' do
66
+ supervisor = Supervisor.new(worker: worker)
67
+ # must stub AFTER adding or else #add_worker will reject
68
+ worker.should_receive(:run).with(no_args())
69
+ t = Thread.new{ supervisor.run }
70
+ sleep(0.1)
71
+ supervisor.stop
72
+ Thread.kill(t)
73
+ end
74
+
75
+ it 'sets the state to running' do
76
+ t = Thread.new{ subject.run }
77
+ sleep(0.1)
78
+ subject.should be_running
79
+ subject.stop
80
+ Thread.kill(t)
81
+ end
82
+
83
+ it 'raises an exception when already running' do
84
+ @thread = nil
85
+ subject.run!
86
+ lambda {
87
+ @thread = Thread.new{ subject.run }
88
+ @thread.abort_on_exception = true
89
+ sleep(0.1)
90
+ }.should raise_error(StandardError)
91
+ subject.stop
92
+ Thread.kill(@thread) unless @thread.nil?
93
+ end
94
+ end
95
+
96
+ context '#run!' do
97
+
98
+ it 'runs the monitor thread' do
99
+ Thread.should_receive(:new).with(no_args())
100
+ subject.run!
101
+ end
102
+
103
+ it 'calls #run on all workers' do
104
+ supervisor = Supervisor.new(worker: worker)
105
+ # must stub AFTER adding or else #add_worker will reject
106
+ worker.should_receive(:run).with(no_args())
107
+ supervisor.run!
108
+ sleep(0.1)
109
+ end
110
+
111
+ it 'sets the state to running' do
112
+ subject.run!
113
+ subject.should be_running
114
+ end
115
+
116
+ it 'raises an exception when already running' do
117
+ subject.run!
118
+ lambda {
119
+ subject.run!
120
+ }.should raise_error(StandardError)
121
+ end
122
+ end
123
+
124
+ context '#stop' do
125
+
126
+ it 'stops the monitor thread' do
127
+ Thread.should_receive(:kill).with(anything())
128
+ subject.run!
129
+ sleep(0.1)
130
+ subject.stop
131
+ end
132
+
133
+ it 'calls #stop on all workers' do
134
+ workers = (1..3).collect{ worker_class.new }
135
+ workers.each{|worker| subject.add_worker(worker)}
136
+ # must stub AFTER adding or else #add_worker will reject
137
+ workers.each{|worker| worker.should_receive(:stop).with(no_args())}
138
+ subject.run!
139
+ sleep(0.1)
140
+ subject.stop
141
+ end
142
+
143
+ it 'sets the state to stopped' do
144
+ subject.run!
145
+ subject.stop
146
+ subject.should_not be_running
147
+ end
148
+
149
+ it 'returns true immediately when already stopped' do
150
+ subject.stop.should be_true
151
+ end
152
+ end
153
+
154
+ context '#running?' do
155
+
156
+ it 'returns true when running' do
157
+ subject.run!
158
+ subject.should be_running
159
+ end
160
+
161
+ it 'returns false when stopped' do
162
+ subject.run!
163
+ subject.stop
164
+ subject.should_not be_running
165
+ end
166
+ end
167
+
168
+ context '#length' do
169
+
170
+ it 'returns a count of attached workers' do
171
+ workers = (1..3).collect{ worker.dup }
172
+ workers.each{|worker| subject.add_worker(worker)}
173
+ subject.length.should == 3
174
+ end
175
+ end
176
+
177
+ context '#add_worker' do
178
+
179
+ it 'adds the worker when stopped' do
180
+ subject.add_worker(worker)
181
+ subject.length.should == 1
182
+ end
183
+
184
+ it 'rejects the worker when running' do
185
+ subject.run!
186
+ subject.add_worker(worker)
187
+ subject.length.should == 0
188
+ end
189
+
190
+ it 'rejects a worker without the :runnable behavior' do
191
+ subject.add_worker('bogus worker')
192
+ subject.length.should == 0
193
+ end
194
+
195
+ it 'returns true when a worker is accepted' do
196
+ subject.add_worker(worker).should be_true
197
+ end
198
+
199
+ it 'returns false when a worker is not accepted' do
200
+ subject.add_worker('bogus worker').should be_false
201
+ end
202
+ end
203
+
204
+ context 'supervision' do
205
+
206
+ it 'reruns any worker that stops' do
207
+ worker = Class.new(worker_class){
208
+ def run() sleep(0.2); end
209
+ }.new
210
+
211
+ supervisor = Supervisor.new(worker: worker, monitor: 0.1)
212
+ supervisor.add_worker(worker)
213
+ # must stub AFTER adding or else #add_worker will reject
214
+ worker.should_receive(:run).with(no_args()).at_least(2).times
215
+ supervisor.run!
216
+ sleep(1)
217
+ supervisor.stop
218
+ end
219
+
220
+ it 'reruns any dead threads' do
221
+ worker = Class.new(worker_class){
222
+ def run() raise StandardError; end
223
+ }.new
224
+
225
+ supervisor = Supervisor.new(worker: worker, monitor: 0.1)
226
+ supervisor.add_worker(worker)
227
+ # must stub AFTER adding or else #add_worker will reject
228
+ worker.should_receive(:run).with(no_args()).at_least(2).times
229
+ supervisor.run!
230
+ sleep(1)
231
+ supervisor.stop
232
+ end
233
+ end
234
+
235
+ context 'supervisor tree' do
236
+
237
+ specify do
238
+ s1 = Supervisor.new(monitor: 0.1)
239
+ s2 = Supervisor.new(monitor: 0.1)
240
+ s3 = Supervisor.new(monitor: 0.1)
241
+
242
+ workers = (1..3).collect{ worker_class.new }
243
+ workers.each{|worker| s3.add_worker(worker)}
244
+ # must stub AFTER adding or else #add_worker will reject
245
+ workers.each{|worker| worker.should_receive(:run).with(no_args())}
246
+ workers.each{|worker| worker.should_receive(:stop).with(no_args())}
247
+
248
+ s1.add_worker(s2)
249
+ s2.add_worker(s3)
250
+
251
+ s1.run!
252
+ sleep(0.1)
253
+ s1.stop
254
+ sleep(0.1)
255
+ end
256
+ end
257
+ end
258
+ end