concurrent-ruby 0.3.2 → 0.4.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.
@@ -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