concurrent-ruby 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,8 +2,20 @@ require 'thread'
2
2
 
3
3
  module Concurrent
4
4
 
5
+ # Error raised when an operations times out.
5
6
  TimeoutError = Class.new(StandardError)
6
7
 
8
+ # Wait the given number of seconds for the block operation to complete.
9
+ #
10
+ # @param [Integer] seconds The number of seconds to wait
11
+ #
12
+ # @return The result of the block operation
13
+ #
14
+ # @raise Concurrent::TimeoutError when the block operation does not complete
15
+ # in the allotted number of seconds.
16
+ #
17
+ # @note This method is intended to be a simpler and more reliable replacement
18
+ # to the Ruby standard library `Timeout::timeout` method.
7
19
  def timeout(seconds)
8
20
 
9
21
  thread = Thread.new do
@@ -20,5 +32,4 @@ module Concurrent
20
32
  Thread.kill(thread) unless thread.nil?
21
33
  end
22
34
  module_function :timeout
23
-
24
35
  end
@@ -1,3 +1,3 @@
1
1
  module Concurrent
2
- VERSION = '0.3.2'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -29,7 +29,7 @@ Actors are defined by subclassing the `Concurrent::Actor` class and overriding t
29
29
  `#act` method. The `#act` method can have any signature/arity but
30
30
 
31
31
  ```ruby
32
- def act(*args, &block)
32
+ def act(*args)
33
33
  ```
34
34
 
35
35
  is the most flexible and least error-prone signature. The `#act` method is called in
@@ -146,7 +146,7 @@ to confusion and difficult debugging.
146
146
  ### Observation
147
147
 
148
148
  The `Actor` superclass mixes in the Ruby standard library
149
- [Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html)
149
+ [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html)
150
150
  module to provide consistent callbacks upon message processing completion. The normal
151
151
  `Observable` methods, including `#add_observer` behave normally. Once an observer
152
152
  is added to an `Actor` it will be notified of all messages processed *after*
@@ -22,7 +22,7 @@ seacrhed in order until one matching the current exception is found. That `rescu
22
22
  then be called an passed the exception object. If no matching `rescue` block is found, or none
23
23
  were configured, then the exception will be suppressed.
24
24
 
25
- `Agent`s also implement Ruby's [Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html).
25
+ `Agent`s also implement Ruby's [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html).
26
26
  Code that observes an `Agent` will receive a callback with the new value any time the value
27
27
  is changed.
28
28
 
@@ -0,0 +1,40 @@
1
+ # Change the dang channel!
2
+
3
+ `Channel` is a functional programming variation of `Actor`, based very loosely on the
4
+ [MailboxProcessor](http://blogs.msdn.com/b/dsyme/archive/2010/02/15/async-and-parallel-design-patterns-in-f-part-3-agents.aspx)
5
+ agent in [F#](http://msdn.microsoft.com/en-us/library/ee370357.aspx).
6
+ The `Actor` is used to create objects that receive messages from other
7
+ threads then processes those messages based on the behavior of the class. `Channel`
8
+ creates objects that receive messages and processe them using the block given
9
+ at construction. `Channel` is implemented as a subclass of
10
+ [Actor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/actor.md)
11
+ and supports all message-passing methods of that class. `Channel` also supports pools
12
+ with a shared mailbox.
13
+
14
+ See the [Actor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/actor.md)
15
+ documentation for more detail.
16
+
17
+ ## Usage
18
+
19
+ ```ruby
20
+ require 'concurrent'
21
+
22
+ channel = Concurrent::Channel.new do |msg|
23
+ sleep(1)
24
+ puts "#{msg}\n"
25
+ end
26
+
27
+ channel.run! => #<Thread:0x007fa123d95fc8 sleep>
28
+
29
+ channel.post("Hello, World!") => 1
30
+ # wait...
31
+ => Hello, World!
32
+
33
+ future = channel.post? "Don't Panic." => #<Concurrent::Contract:0x007fa123d6d9d8 @state=:pending...
34
+ future.pending? => true
35
+ # wait...
36
+ => "Don't Panic."
37
+ future.fulfilled? => true
38
+
39
+ channel.stop => true
40
+ ```
@@ -2,18 +2,16 @@
2
2
 
3
3
  Object references in Ruby are mutable. This can lead to serious problems when
4
4
  the `#value` of a concurrent object is a mutable reference. Which is always the
5
- case unless the value is a `Fixnum`, `Symbol`, or similar "primative" data type.
5
+ case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type.
6
6
  Most classes in this library that expose a `#value` getter method do so using
7
- the Dereferenceable mixin module.
7
+ the `Dereferenceable` mixin module.
8
8
 
9
+ Objects with this mixin can be configured with a few options that can help protect
10
+ the program from potentially dangerous operations.
9
11
 
10
- Each `Agent` instance can be configured with a few options that can help protect the
11
- program from potentially dangerous operations. Each of these options can be
12
- optionally set when the `Agent` is created:
13
-
14
- * `:dup_on_deref` when true the `Agent` will call the `#dup` method on the
15
- `value` object every time the `#value` methid is called (default: false)
16
- * `:freeze_on_deref` when true the `Agent` will call the `#freeze` method on the
12
+ * `:dup_on_deref` when true will call the `#dup` method on the
13
+ `value` object every time the `#value` method is called (default: false)
14
+ * `:freeze_on_deref` when true will call the `#freeze` method on the
17
15
  `value` object every time the `#value` method is called (default: false)
18
16
  * `:copy_on_deref` when given a `Proc` object the `Proc` will be run every time
19
17
  the `#value` method is called. The `Proc` will be given the current `value` as
@@ -22,7 +22,7 @@ block indefinitely. If `0` the call will not block. Any other integer or float v
22
22
  maximum number of seconds to block.
23
23
 
24
24
  The `Future` class also includes the Ruby standard library
25
- [Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html) module. On fulfillment
25
+ [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html) module. On fulfillment
26
26
  or rejection all observers will be notified according to the normal `Observable` behavior. The observer
27
27
  callback function will be called with three parameters: the `Time` of fulfillment/rejection, the
28
28
  final `value`, and the final `reason`. Observers added after fulfillment/rejection will still be
@@ -35,7 +35,7 @@ A fulfilled example:
35
35
  ```ruby
36
36
  require 'concurrent'
37
37
 
38
- count = Concurrent::Future{ sleep(10); 10 }
38
+ count = Concurrent::Future.new{ sleep(10); 10 }
39
39
  count.state #=> :pending
40
40
  count.pending? #=> true
41
41
 
@@ -15,7 +15,7 @@ upon resolution. When a promise is rejected all its children will be summarily r
15
15
 
16
16
  Promises have three possible states: *pending*, *rejected*, and *fulfilled*. When a promise is created it is set
17
17
  to *pending* and will remain in that state until processing is complete. A completed promise is either *rejected*,
18
- indicating that an exception was thrown during processing, or *fulfilled*, indicating succedd. If a promise is
18
+ indicating that an exception was thrown during processing, or *fulfilled*, indicating it succeeded. If a promise is
19
19
  *fulfilled* its `value` will be updated to reflect the result of the operation. If *rejected* the `reason` will
20
20
  be updated with a reference to the thrown exception. The predicate methods `pending?`, `rejected`, and `fulfilled?`
21
21
  can be called at any time to obtain the state of the promise, as can the `state` method, which returns a symbol.
@@ -45,7 +45,7 @@ The result of a `ScheduledTask` can be obtained either synchronously or asynchro
45
45
  `ScheduledTask` mixes in both the
46
46
  [Obligation](https://github.com/jdantonio/concurrent-ruby/blob/master/md/obligation.md)
47
47
  module and the
48
- [Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html)
48
+ [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html)
49
49
  module from the Ruby standard library. With one exception `ScheduledTask` behaves
50
50
  identically to
51
51
  [Concurrent::Future](https://github.com/jdantonio/concurrent-ruby/blob/master/md/future.md)
@@ -31,7 +31,7 @@ This class is based on the Java class
31
31
  ## Observation
32
32
 
33
33
  `TimerTask` supports notification through the Ruby standard library
34
- [Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html)
34
+ [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html)
35
35
  module. On execution the `TimerTask` will notify the observers with thress arguments:
36
36
  time of execution, the result of the block (or nil on failure), and any raised
37
37
  exceptions (or nil on success). If the timeout interval is exceeded the observer
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require_relative 'postable_shared'
2
3
  require_relative 'runnable_shared'
3
4
 
4
5
  module Concurrent
@@ -19,242 +20,30 @@ module Concurrent
19
20
  end
20
21
  end
21
22
 
23
+ ## :runnable
22
24
  subject { Class.new(actor_class).new }
23
-
24
25
  it_should_behave_like :runnable
25
26
 
26
- after(:each) do
27
- subject.stop
28
- @thread.kill unless @thread.nil?
29
- sleep(0.1)
30
- end
31
-
32
- context '#post' do
33
-
34
- it 'returns false when not running' do
35
- subject.post.should be_false
36
- end
27
+ ## :postable
37
28
 
38
- it 'pushes a message onto the queue' do
39
- @expected = false
40
- actor = actor_class.new{|msg| @expected = msg }
41
- @thread = Thread.new{ actor.run }
42
- @thread.join(0.1)
43
- actor.post(true)
44
- @thread.join(0.1)
45
- @expected.should be_true
46
- actor.stop
47
- end
29
+ let!(:postable_class){ actor_class }
48
30
 
49
- it 'returns the current size of the queue' do
50
- actor = actor_class.new{|msg| sleep }
51
- @thread = Thread.new{ actor.run }
52
- @thread.join(0.1)
53
- actor.post(true).should == 1
54
- @thread.join(0.1)
55
- actor.post(true).should == 1
56
- @thread.join(0.1)
57
- actor.post(true).should == 2
58
- actor.stop
59
- end
60
-
61
- it 'is aliased a <<' do
62
- @expected = false
63
- actor = actor_class.new{|msg| @expected = msg }
64
- @thread = Thread.new{ actor.run }
65
- @thread.join(0.1)
66
- actor << true
67
- @thread.join(0.1)
68
- @expected.should be_true
69
- actor.stop
70
- end
71
- end
72
-
73
- context '#post?' do
74
-
75
- it 'returns nil when not running' do
76
- subject.post?.should be_false
77
- end
78
-
79
- it 'returns an Obligation' do
80
- actor = actor_class.new
81
- @thread = Thread.new{ actor.run }
82
- @thread.join(0.1)
83
- obligation = actor.post?(nil)
84
- obligation.should be_a(Obligation)
85
- actor.stop
86
- end
87
-
88
- it 'fulfills the obligation on success' do
89
- actor = actor_class.new{|msg| @expected = msg }
90
- @thread = Thread.new{ actor.run }
91
- @thread.join(0.1)
92
- obligation = actor.post?(42)
93
- @thread.join(0.1)
94
- obligation.should be_fulfilled
95
- obligation.value.should == 42
96
- actor.stop
97
- end
98
-
99
- it 'rejects the obligation on failure' do
100
- actor = actor_class.new{|msg| raise StandardError.new('Boom!') }
101
- @thread = Thread.new{ actor.run }
102
- @thread.join(0.1)
103
- obligation = actor.post?(42)
104
- @thread.join(0.1)
105
- obligation.should be_rejected
106
- obligation.reason.should be_a(StandardError)
107
- actor.stop
108
- end
109
- end
110
-
111
- context '#post!' do
112
-
113
- it 'raises Concurrent::Runnable::LifecycleError when not running' do
114
- expect {
115
- subject.post!(1)
116
- }.to raise_error(Concurrent::Runnable::LifecycleError)
117
- end
118
-
119
- it 'blocks for up to the given number of seconds' do
120
- actor = actor_class.new{|msg| sleep }
121
- @thread = Thread.new{ actor.run }
122
- @thread.join(0.1)
123
- start = Time.now.to_i
124
- expect {
125
- actor.post!(2, nil)
126
- }.to raise_error
127
- elapsed = Time.now.to_i - start
128
- elapsed.should >= 2
129
- actor.stop
130
- end
131
-
132
- it 'raises Concurrent::TimeoutError when seconds is zero' do
133
- actor = actor_class.new{|msg| 42 }
134
- @thread = Thread.new{ actor.run }
135
- @thread.join(0.1)
136
- expect {
137
- actor.post!(0, nil)
138
- }.to raise_error(Concurrent::TimeoutError)
139
- actor.stop
140
- end
141
-
142
- it 'raises Concurrent::TimeoutError on timeout' do
143
- actor = actor_class.new{|msg| sleep }
144
- @thread = Thread.new{ actor.run }
145
- @thread.join(0.1)
146
- expect {
147
- actor.post!(1, nil)
148
- }.to raise_error(Concurrent::TimeoutError)
149
- actor.stop
150
- end
151
-
152
- it 'bubbles the exception on error' do
153
- actor = actor_class.new{|msg| raise StandardError.new('Boom!') }
154
- @thread = Thread.new{ actor.run }
155
- @thread.join(0.1)
156
- expect {
157
- actor.post!(1, nil)
158
- }.to raise_error(StandardError)
159
- actor.stop
160
- end
161
-
162
- it 'returns the result on success' do
163
- actor = actor_class.new{|msg| 42 }
164
- @thread = Thread.new{ actor.run }
165
- @thread.join(0.1)
166
- expected = actor.post!(1, nil)
167
- expected.should == 42
168
- actor.stop
169
- end
170
-
171
- it 'attempts to cancel the operation on timeout' do
172
- @expected = 0
173
- actor = actor_class.new{|msg| sleep(0.5); @expected += 1 }
174
- @thread = Thread.new{ actor.run }
175
- @thread.join(0.1)
176
- actor.post(nil) # block the actor
177
- expect {
178
- actor.post!(0.1, nil)
179
- }.to raise_error(Concurrent::TimeoutError)
180
- sleep(1.5)
181
- @expected.should == 1
182
- actor.stop
183
- end
184
- end
185
-
186
- context '#forward' do
187
-
188
- let(:sender_clazz) do
189
- Class.new(Actor) do
190
- def act(*message)
191
- if message.first.is_a?(Exception)
192
- raise message.first
193
- else
194
- return message.first
195
- end
196
- end
197
- end
198
- end
199
-
200
- let(:receiver_clazz) do
201
- Class.new(Actor) do
202
- attr_reader :result
203
- def act(*message)
204
- @result = message.first
31
+ let(:sender_class) do
32
+ Class.new(Actor) do
33
+ def act(*message)
34
+ if message.first.is_a?(Exception)
35
+ raise message.first
36
+ else
37
+ return message.first
205
38
  end
206
39
  end
207
40
  end
41
+ end
208
42
 
209
- let(:sender) { sender_clazz.new }
210
- let(:receiver) { receiver_clazz.new }
211
-
212
- let(:observer) { double('observer') }
213
-
214
- before(:each) do
215
- @sender = Thread.new{ sender.run }
216
- @receiver = Thread.new{ receiver.run }
217
- sleep(0.1)
218
- end
219
-
220
- after(:each) do
221
- sender.stop
222
- receiver.stop
223
- sleep(0.1)
224
- @sender.kill unless @sender.nil?
225
- @receiver.kill unless @receiver.nil?
226
- end
227
-
228
- it 'returns false when sender not running' do
229
- sender_clazz.new.forward(receiver).should be_false
230
- end
231
-
232
- it 'forwards the result to the receiver on success' do
233
- sender.forward(receiver, 42)
234
- sleep(0.1)
235
- receiver.result.should eq 42
236
- end
43
+ let(:sender) { sender_class.new }
44
+ let(:receiver) { postable_class.new }
237
45
 
238
- it 'does not forward on exception' do
239
- sender.forward(receiver, StandardError.new)
240
- sleep(0.1)
241
- receiver.result.should be_nil
242
- end
243
-
244
- it 'notifies observers on success' do
245
- observer.should_receive(:update).with(any_args())
246
- sender.add_observer(observer)
247
- sender.forward(receiver, 42)
248
- sleep(0.1)
249
- end
250
-
251
- it 'notifies observers on exception' do
252
- observer.should_not_receive(:update).with(any_args())
253
- sender.add_observer(observer)
254
- sender.forward(receiver, StandardError.new)
255
- sleep(0.1)
256
- end
257
- end
46
+ it_should_behave_like :postable
258
47
 
259
48
  context '#run' do
260
49
 
@@ -394,65 +183,69 @@ module Concurrent
394
183
  }.to raise_error(ArgumentError)
395
184
  end
396
185
 
397
- it 'creates the requested number of actors' do
398
- mailbox, actors = clazz.pool(5)
399
- actors.size.should == 5
186
+ it 'creates the requested number of pool' do
187
+ mailbox, pool = clazz.pool(5)
188
+ pool.size.should == 5
189
+ end
190
+
191
+ it 'passes all optional arguments to the individual constructors' do
192
+ clazz.should_receive(:new).with(1, 2, 3).exactly(5).times
193
+ clazz.pool(5, 1, 2, 3)
400
194
  end
401
195
 
402
- it 'passes the block to each actor' do
196
+ it 'passes a duplicate of the given block to each actor in the pool' do
403
197
  block = proc{ nil }
404
- clazz.should_receive(:new).with(&block)
405
- clazz.pool(1, &block)
198
+ block.should_receive(:dup).exactly(5).times.and_return(proc{ nil })
199
+ mailbox, pool = Channel.pool(5, &block)
406
200
  end
407
201
 
408
- it 'gives all actors the same mailbox' do
409
- mailbox, actors = clazz.pool(2)
410
- mbox1 = actors.first.instance_variable_get(:@queue)
411
- mbox2 = actors.last.instance_variable_get(:@queue)
202
+ it 'gives all pool the same mailbox' do
203
+ mailbox, pool = clazz.pool(2)
204
+ mbox1 = pool.first.instance_variable_get(:@queue)
205
+ mbox2 = pool.last.instance_variable_get(:@queue)
412
206
  mbox1.should eq mbox2
413
207
  end
414
208
 
415
209
  it 'returns a Poolbox as the first retval' do
416
- mailbox, actors = clazz.pool(2)
210
+ mailbox, pool = clazz.pool(2)
417
211
  mailbox.should be_a(Actor::Poolbox)
418
212
  end
419
213
 
420
- it 'gives the Poolbox the same mailbox as the actors' do
421
- mailbox, actors = clazz.pool(1)
214
+ it 'gives the Poolbox the same mailbox as the pool' do
215
+ mailbox, pool = clazz.pool(1)
422
216
  mbox1 = mailbox.instance_variable_get(:@queue)
423
- mbox2 = actors.first.instance_variable_get(:@queue)
217
+ mbox2 = pool.first.instance_variable_get(:@queue)
424
218
  mbox1.should eq mbox2
425
219
  end
426
220
 
427
- it 'returns an array of actors as the second retval' do
428
- mailbox, actors = clazz.pool(2)
429
- actors.each do |actor|
221
+ it 'returns an array of pool as the second retval' do
222
+ mailbox, pool = clazz.pool(2)
223
+ pool.each do |actor|
430
224
  actor.should be_a(clazz)
431
225
  end
432
226
  end
433
227
 
434
228
  it 'posts to the mailbox with Poolbox#post' do
435
- @expected = false
436
- mailbox, actors = clazz.pool(1){|msg| @expected = true }
437
- @thread = Thread.new{ actors.first.run }
229
+ mailbox, pool = clazz.pool(1)
230
+ @thread = Thread.new{ pool.first.run }
438
231
  sleep(0.1)
439
232
  mailbox.post(42)
440
233
  sleep(0.1)
441
- actors.each{|actor| actor.stop }
234
+ pool.first.last_message.should eq [42]
235
+ pool.first.stop
442
236
  @thread.kill
443
- @expected.should be_true
444
237
  end
445
238
 
446
239
  it 'posts to the mailbox with Poolbox#<<' do
447
240
  @expected = false
448
- mailbox, actors = clazz.pool(1){|msg| @expected = true }
449
- @thread = Thread.new{ actors.first.run }
241
+ mailbox, pool = clazz.pool(1)
242
+ @thread = Thread.new{ pool.first.run }
450
243
  sleep(0.1)
451
244
  mailbox << 42
452
245
  sleep(0.1)
453
- actors.each{|actor| actor.stop }
246
+ pool.first.last_message.should eq [42]
247
+ pool.first.stop
454
248
  @thread.kill
455
- @expected.should be_true
456
249
  end
457
250
  end
458
251
 
@@ -464,10 +257,10 @@ module Concurrent
464
257
 
465
258
  context '#pool' do
466
259
 
467
- it 'creates actors of the appropriate subclass' do
260
+ it 'creates pool of the appropriate subclass' do
468
261
  actor = Class.new(actor_class)
469
- mailbox, actors = actor.pool(1)
470
- actors.first.should be_a(actor)
262
+ mailbox, pool = actor.pool(1)
263
+ pool.first.should be_a(actor)
471
264
  end
472
265
  end
473
266