concurrent-ruby 0.1.1 → 0.2.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.
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