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
@@ -167,26 +167,6 @@ module Concurrent
167
167
  end
168
168
  end
169
169
 
170
- describe Defer do
171
-
172
- before(:each) do
173
- Defer.thread_pool = FixedThreadPool.new(1)
174
- end
175
-
176
- it 'aliases Kernel#defer' do
177
- defer{ nil }.should be_a(Defer)
178
- end
179
- end
180
-
181
- describe Executor do
182
-
183
- it 'aliases Kernel#executor' do
184
- ex = executor('executor'){ nil }
185
- ex.should be_a(Executor::ExecutionContext)
186
- ex.kill
187
- end
188
- end
189
-
190
170
  describe Future do
191
171
 
192
172
  before(:each) do
@@ -108,5 +108,93 @@ module Concurrent
108
108
  end
109
109
  end
110
110
  end
111
+
112
+ context 'observation' do
113
+
114
+ let(:clazz) do
115
+ Class.new do
116
+ attr_reader :value
117
+ attr_reader :reason
118
+ attr_reader :count
119
+ define_method(:update) do |time, value, reason|
120
+ @count = @count.to_i + 1
121
+ @value = value
122
+ @reason = reason
123
+ end
124
+ end
125
+ end
126
+
127
+ let(:observer) { clazz.new }
128
+
129
+ it 'notifies all observers on fulfillment' do
130
+ future = Future.new{ sleep(0.1); 42 }
131
+ future.add_observer(observer)
132
+ future.value.should == 42
133
+ future.reason.should be_nil
134
+ sleep(0.1)
135
+ observer.value.should == 42
136
+ observer.reason.should be_nil
137
+ end
138
+
139
+ it 'notifies all observers on rejection' do
140
+ future = Future.new{ sleep(0.1); raise StandardError }
141
+ future.add_observer(observer)
142
+ future.value.should be_nil
143
+ future.reason.should be_a(StandardError)
144
+ sleep(0.1)
145
+ observer.value.should be_nil
146
+ observer.reason.should be_a(StandardError)
147
+ end
148
+
149
+ it 'notifies an observer added after fulfillment' do
150
+ future = Future.new{ 42 }
151
+ sleep(0.1)
152
+ future.value.should == 42
153
+ future.add_observer(observer)
154
+ observer.value.should be_nil
155
+ sleep(0.1)
156
+ observer.value.should == 42
157
+ end
158
+
159
+ it 'notifies an observer added after rejection' do
160
+ future = Future.new{ raise StandardError }
161
+ sleep(0.1)
162
+ future.reason.should be_a(StandardError)
163
+ future.add_observer(observer)
164
+ observer.value.should be_nil
165
+ sleep(0.1)
166
+ observer.reason.should be_a(StandardError)
167
+ end
168
+
169
+ it 'does not notify existing observers when a new observer added after fulfillment' do
170
+ future = Future.new{ 42 }
171
+ future.add_observer(observer)
172
+ sleep(0.1)
173
+ future.value.should == 42
174
+ observer.count.should == 1
175
+
176
+ o2 = clazz.new
177
+ future.add_observer(o2)
178
+ sleep(0.1)
179
+
180
+ observer.count.should == 1
181
+ o2.value.should == 42
182
+ end
183
+
184
+ it 'does not notify existing observers when a new observer added after rejection' do
185
+ future = Future.new{ raise StandardError }
186
+ future.add_observer(observer)
187
+ sleep(0.1)
188
+ future.reason.should be_a(StandardError)
189
+ observer.count.should == 1
190
+
191
+ o2 = clazz.new
192
+ future.add_observer(o2)
193
+ sleep(0.1)
194
+
195
+ observer.count.should == 1
196
+ o2.reason.should be_a(StandardError)
197
+ end
198
+ end
111
199
  end
112
200
  end
@@ -14,20 +14,41 @@ module Concurrent
14
14
 
15
15
  context '#post' do
16
16
 
17
- it 'proxies a call without arguments' do
17
+ it 'creates a new thread for a call without arguments' do
18
18
  thread = Thread.new{ nil }
19
19
  Thread.should_receive(:new).with(no_args()).and_return(thread)
20
20
  $GLOBAL_THREAD_POOL.should_not_receive(:post).with(any_args())
21
21
  subject.post{ nil }
22
22
  end
23
23
 
24
- it 'proxies a call with arguments' do
24
+ it 'executes a call without arguments' do
25
+ @expected = false
26
+ subject.post{ @expected = true }
27
+ sleep(0.1)
28
+ @expected.should be_true
29
+ end
30
+
31
+ it 'creates a new thread for a call with arguments' do
25
32
  thread = Thread.new{ nil }
26
33
  Thread.should_receive(:new).with(1,2,3).and_return(thread)
27
34
  $GLOBAL_THREAD_POOL.should_not_receive(:post).with(any_args())
28
35
  subject.post(1,2,3){ nil }
29
36
  end
30
37
 
38
+ it 'executes a call with one argument' do
39
+ @expected = 0
40
+ subject.post(1){|one| @expected = one }
41
+ sleep(0.1)
42
+ @expected.should == 1
43
+ end
44
+
45
+ it 'executes a call with multiple arguments' do
46
+ @expected = nil
47
+ subject.post(1,2,3,4,5){|*args| @expected = args }
48
+ sleep(0.1)
49
+ @expected.should eq [1,2,3,4,5]
50
+ end
51
+
31
52
  it 'aliases #<<' do
32
53
  thread = Thread.new{ nil }
33
54
  Thread.should_receive(:new).with(no_args()).and_return(thread)
@@ -27,14 +27,12 @@ share_examples_for Concurrent::Obligation do
27
27
 
28
28
  it 'blocks the caller when :pending and timeout is nil' do
29
29
  f = pending_subject
30
- sleep(0.1)
31
30
  f.value.should be_true
32
31
  f.should be_fulfilled
33
32
  end
34
33
 
35
34
  it 'returns nil when reaching the optional timeout value' do
36
35
  f = pending_subject
37
- sleep(0.1)
38
36
  f.value(0).should be_nil
39
37
  f.should be_pending
40
38
  end
@@ -42,21 +40,18 @@ share_examples_for Concurrent::Obligation do
42
40
  it 'returns immediately when timeout is zero' do
43
41
  Timeout.should_not_receive(:timeout).with(any_args())
44
42
  f = pending_subject
45
- sleep(0.1)
46
43
  f.value(0).should be_nil
47
44
  f.should be_pending
48
45
  end
49
46
 
50
47
  it 'returns the value when fulfilled before timeout' do
51
48
  f = pending_subject
52
- sleep(0.1)
53
49
  f.value(10).should be_true
54
50
  f.should be_fulfilled
55
51
  end
56
52
 
57
53
  it 'returns nil when timeout reached' do
58
54
  f = pending_subject
59
- sleep(0.1)
60
55
  f.value(0.1).should be_nil
61
56
  f.should be_pending
62
57
  end
@@ -156,7 +156,7 @@ module Concurrent
156
156
  it 'sets the promise reason the error object on exception' do
157
157
  p = Promise.new{ raise StandardError.new('Boom!') }
158
158
  sleep(0.1)
159
- p.reason.should be_a(Exception)
159
+ p.reason.should be_a(StandardError)
160
160
  p.reason.should.to_s =~ /Boom!/
161
161
  end
162
162
 
@@ -207,7 +207,7 @@ module Concurrent
207
207
  Promise.new{ raise ArgumentError }.
208
208
  rescue(ArgumentError){|ex| @expected = 1 }.
209
209
  rescue(LoadError){|ex| @expected = 2 }.
210
- rescue(Exception){|ex| @expected = 3 }
210
+ rescue(StandardError){|ex| @expected = 3 }
211
211
  sleep(0.1)
212
212
  @expected.should eq 1
213
213
 
@@ -215,7 +215,7 @@ module Concurrent
215
215
  Promise.new{ raise LoadError }.
216
216
  rescue(ArgumentError){|ex| @expected = 1 }.
217
217
  rescue(LoadError){|ex| @expected = 2 }.
218
- rescue(Exception){|ex| @expected = 3 }
218
+ rescue(StandardError){|ex| @expected = 3 }
219
219
  sleep(0.1)
220
220
  @expected.should eq 2
221
221
 
@@ -223,7 +223,7 @@ module Concurrent
223
223
  Promise.new{ raise StandardError }.
224
224
  rescue(ArgumentError){|ex| @expected = 1 }.
225
225
  rescue(LoadError){|ex| @expected = 2 }.
226
- rescue(Exception){|ex| @expected = 3 }
226
+ rescue(StandardError){|ex| @expected = 3 }
227
227
  sleep(0.1)
228
228
  @expected.should eq 3
229
229
  end
@@ -233,7 +233,7 @@ module Concurrent
233
233
  Promise.new{ raise StandardError }.
234
234
  rescue(ArgumentError){|ex| @expected = ex }.
235
235
  rescue(LoadError){|ex| @expected = ex }.
236
- rescue(Exception){|ex| @expected = ex }
236
+ rescue(StandardError){|ex| @expected = ex }
237
237
  sleep(0.1)
238
238
  @expected.should be_a(StandardError)
239
239
  end
@@ -242,8 +242,7 @@ module Concurrent
242
242
  @expected = nil
243
243
  Promise.new{ raise StandardError }.
244
244
  rescue(StandardError).
245
- rescue(StandardError){|ex| @expected = ex }.
246
- rescue(Exception){|ex| @expected = ex }
245
+ rescue(StandardError){|ex| @expected = ex }
247
246
  sleep(0.1)
248
247
  @expected.should be_a(StandardError)
249
248
  end
@@ -252,8 +251,8 @@ module Concurrent
252
251
  lambda {
253
252
  Promise.new{ raise StandardError }.
254
253
  rescue(ArgumentError){|ex| @expected = ex }.
255
- rescue(StandardError){|ex| @expected = ex }.
256
- rescue(Exception){|ex| @expected = ex }
254
+ rescue(NotImplementedError){|ex| @expected = ex }.
255
+ rescue(NoMethodError){|ex| @expected = ex }
257
256
  sleep(0.1)
258
257
  }.should_not raise_error
259
258
  end
@@ -261,7 +260,7 @@ module Concurrent
261
260
  it 'supresses exceptions thrown from rescue handlers' do
262
261
  lambda {
263
262
  Promise.new{ raise ArgumentError }.
264
- rescue(Exception){ raise StandardError }
263
+ rescue(StandardError){ raise StandardError }
265
264
  sleep(0.1)
266
265
  }.should_not raise_error
267
266
  end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ share_examples_for :runnable do
4
+
5
+ after(:each) do
6
+ subject.stop
7
+ @thread.kill unless @thread.nil?
8
+ sleep(0.1)
9
+ end
10
+
11
+ context '#run' do
12
+
13
+ it 'starts the (blocking) runner on the current thread when stopped' do
14
+ @thread = Thread.new { subject.run }
15
+ @thread.join(1).should be_nil
16
+ end
17
+
18
+ it 'raises an exception when already running' do
19
+ @thread = Thread.new { subject.run }
20
+ @thread.join(0.1)
21
+ expect {
22
+ subject.run
23
+ }.to raise_error
24
+ end
25
+
26
+ it 'returns true when stopped normally' do
27
+ @expected = false
28
+ @thread = Thread.new { @expected = subject.run }
29
+ @thread.join(0.1)
30
+ subject.stop
31
+ @thread.join(1)
32
+ @expected.should be_true
33
+ end
34
+ end
35
+
36
+ context '#stop' do
37
+
38
+ it 'returns true when not running' do
39
+ subject.stop.should be_true
40
+ end
41
+
42
+ it 'returns true when successfully stopped' do
43
+ @thread = Thread.new { subject.run }
44
+ @thread.join(0.1)
45
+ subject.stop.should be_true
46
+ subject.should_not be_running
47
+ end
48
+ end
49
+
50
+ context '#running?' do
51
+
52
+ it 'returns true when running' do
53
+ @thread = Thread.new { subject.run }
54
+ @thread.join(0.1)
55
+ subject.should be_running
56
+ end
57
+
58
+ it 'returns false when not running' do
59
+ subject.should_not be_running
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,233 @@
1
+ require 'spec_helper'
2
+ require_relative 'runnable_shared'
3
+
4
+ module Concurrent
5
+
6
+ describe Runnable do
7
+
8
+ let(:runnable) do
9
+ Class.new {
10
+ include Runnable
11
+ attr_reader :thread
12
+ def initialize(*args, &block)
13
+ yield if block_given?
14
+ end
15
+ def on_task
16
+ @thread = Thread.current
17
+ sleep(0.1)
18
+ end
19
+ def on_run() return true; end
20
+ def after_run() return true; end
21
+ def on_stop() return true; end
22
+ }
23
+ end
24
+
25
+ subject { runnable.new }
26
+
27
+ it_should_behave_like :runnable
28
+
29
+ after(:each) do
30
+ subject.stop
31
+ @thread.kill unless @thread.nil?
32
+ sleep(0.1)
33
+ end
34
+
35
+ context '#run' do
36
+
37
+ it 'calls #on_run when implemented' do
38
+ subject.should_receive(:on_run).with(no_args())
39
+ @thread = Thread.new { subject.run }
40
+ sleep(0.1)
41
+ end
42
+
43
+ it 'does not attempt to call #on_run when not implemented' do
44
+ subject.class.send(:remove_method, :on_run)
45
+ @thread = Thread.new do
46
+ expect {
47
+ subject.run
48
+ }.not_to raise_error
49
+ end
50
+ sleep(0.1)
51
+ end
52
+
53
+ it 'return false when #on_run raises an exception' do
54
+ @expected = true
55
+ subject.stub(:on_run).and_raise(StandardError)
56
+ @thread = Thread.new do
57
+ @expected = subject.run
58
+ end
59
+ sleep(0.1)
60
+ @expected.should be_false
61
+ end
62
+
63
+ it 'calls #on_task in an infinite loop' do
64
+ subject.should_receive(:on_task).with(no_args()).at_least(1)
65
+ @thread = Thread.new { subject.run }
66
+ @thread.join(1)
67
+ end
68
+
69
+ it 'raises an exception if the #on_task callback is not implemented' do
70
+ runner = Class.new { include Runnable }.new
71
+ expect {
72
+ runner.run
73
+ }.to raise_error(Runnable::LifecycleError)
74
+ end
75
+
76
+ it 'returns false when the task loop raises an exception' do
77
+ @expected = false
78
+ subject.stub(:on_task).and_raise(StandardError)
79
+ @thread = Thread.new { @expected = subject.run }
80
+ @thread.join(0.1)
81
+ @expected.should be_false
82
+ end
83
+ end
84
+
85
+ context '#stop' do
86
+
87
+ it 'calls #on_stop when implemented' do
88
+ subject.should_receive(:on_stop).with(no_args())
89
+ @thread = Thread.new { subject.run }
90
+ sleep(0.1)
91
+ subject.stop
92
+ sleep(0.1)
93
+ end
94
+
95
+ it 'does not attempt to call #on_stop when not implemented' do
96
+ subject.class.send(:remove_method, :on_stop)
97
+ @thread = Thread.new { subject.run }
98
+ sleep(0.1)
99
+ expect {
100
+ subject.stop
101
+ sleep(0.1)
102
+ }.not_to raise_error
103
+ end
104
+
105
+ it 'return false when #on_stop raises an exception' do
106
+ subject.stub(:on_stop).and_raise(StandardError)
107
+ @thread = Thread.new { subject.run }
108
+ sleep(0.1)
109
+ subject.stop.should be_false
110
+ subject.should_not be_running
111
+ end
112
+
113
+ it 'calls #after_run when implemented' do
114
+ subject.should_receive(:after_run).with(no_args())
115
+ @thread = Thread.new { subject.run }
116
+ sleep(0.1)
117
+ subject.stop
118
+ sleep(0.2)
119
+ end
120
+
121
+ it 'does not attempt to call #after_run when not implemented' do
122
+ subject.class.send(:remove_method, :after_run)
123
+ @thread = Thread.new { subject.run }
124
+ sleep(0.1)
125
+ expect {
126
+ subject.stop
127
+ sleep(0.2)
128
+ }.not_to raise_error
129
+ end
130
+ end
131
+
132
+ context '#running?' do
133
+
134
+ it 'returns false if runner abends' do
135
+ subject.stub(:on_task).and_raise(StandardError)
136
+ @thread = Thread.new { subject.run }
137
+ @thread.join(0.1)
138
+ subject.should_not be_running
139
+ end
140
+ end
141
+
142
+ context 'instance #run!' do
143
+
144
+ let(:clazz) do
145
+ Class.new { include Runnable }
146
+ end
147
+
148
+ subject { clazz.new }
149
+
150
+ after(:each) do
151
+ @context.runner.stop if @context && @context.runner
152
+ @context.thread.kill if @context && @context.thread
153
+ end
154
+
155
+ it 'creates a new thread' do
156
+ Thread.should_receive(:new).with(any_args()).and_return(nil)
157
+ @context = subject.run!
158
+ sleep(0.1)
159
+ end
160
+
161
+ it 'runs the runner on the new thread' do
162
+ @context = subject.run!
163
+ sleep(0.1)
164
+ @context.thread.should_not eq Thread.current
165
+ end
166
+
167
+ it 'returns a context object on success' do
168
+ @context = subject.run!
169
+ sleep(0.1)
170
+ @context.should be_a(Running::Context)
171
+ end
172
+ end
173
+
174
+ context 'module #run!' do
175
+
176
+ let(:clazz) do
177
+ Class.new { include Runnable }
178
+ end
179
+
180
+ after(:each) do
181
+ @context.runner.stop if @context && @context.runner
182
+ @context.thread.kill if @context && @context.thread
183
+ end
184
+
185
+ it 'creates a new runner' do
186
+ clazz.should_receive(:new).once.with(no_args())
187
+ @context = clazz.run!
188
+ sleep(0.1)
189
+ end
190
+
191
+ it 'passes all args to the runner constructor' do
192
+ args = [1, 2, :three, :four]
193
+ clazz.should_receive(:new).once.with(*args)
194
+ @context = clazz.run!(*args)
195
+ sleep(0.1)
196
+ end
197
+
198
+ it 'passes a block argument to the runner constructor' do
199
+ @expected = false
200
+ @context = runnable.run!{ @expected = true }
201
+ sleep(0.1)
202
+ @expected.should be_true
203
+ end
204
+
205
+ it 'creates a new thread' do
206
+ Thread.should_receive(:new).with(any_args()).and_return(nil)
207
+ @context = runnable.run!
208
+ sleep(0.1)
209
+ end
210
+
211
+ it 'runs the runner on the new thread' do
212
+ @context = runnable.run!
213
+ sleep(0.1)
214
+ @context.runner.thread.should_not eq Thread.current
215
+ @context.runner.thread.should eq @context.thread
216
+ @context.thread.should_not eq Thread.current
217
+ end
218
+
219
+ it 'returns a context object on success' do
220
+ @context = runnable.run!
221
+ sleep(0.1)
222
+ @context.should be_a(Running::Context)
223
+ end
224
+
225
+ it 'returns nil on failure' do
226
+ Thread.stub(:new).with(any_args()).and_raise(StandardError)
227
+ @context = runnable.run!
228
+ sleep(0.1)
229
+ @context.should be_nil
230
+ end
231
+ end
232
+ end
233
+ end