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