Skip to content

Commit e89ba53

Browse files
committed
Merge branch 'master' of github.com:jdantonio/concurrent-ruby
2 parents 91f1185 + a4cd69c commit e89ba53

File tree

3 files changed

+65
-98
lines changed

3 files changed

+65
-98
lines changed

lib/concurrent/promise.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ class Promise
1212
# @see http://wiki.commonjs.org/wiki/Promises/A
1313
# @see http://promises-aplus.github.io/promises-spec/
1414
def initialize(options = {}, &block)
15+
options.delete_if {|k, v| v.nil?}
16+
1517
@parent = options.fetch(:parent) { nil }
1618
@on_fulfill = options.fetch(:on_fulfill) { Proc.new{ |result| result } }
17-
@on_reject = options.fetch(:on_reject) { Proc.new{ |result| result } }
19+
@on_reject = options.fetch(:on_reject) { Proc.new{ |reason| raise reason } }
1820

1921
@promise_body = block || Proc.new{|result| result }
2022
@state = :unscheduled

md/promise.md

Lines changed: 49 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@ and [Promises/A+](http://promises-aplus.github.io/promises-spec/) specifications
99
Promises are similar to futures and share many of the same behaviours. Promises are far more robust,
1010
however. Promises can be chained in a tree structure where each promise may have zero or more children.
1111
Promises are chained using the `then` method. The result of a call to `then` is always another promise.
12-
Promises are resolved asynchronously in the order they are added to the tree. Parents are guaranteed
13-
to be resolved before their children. The result of each promise is passed to each of its children
14-
upon resolution. When a promise is rejected all its children will be summarily rejected.
15-
16-
Promises have three possible states: *pending*, *rejected*, and *fulfilled*. When a promise is created it is set
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 it succeeded. If a promise is
19-
*fulfilled* its `value` will be updated to reflect the result of the operation. If *rejected* the `reason` will
20-
be updated with a reference to the thrown exception. The predicate methods `pending?`, `rejected`, and `fulfilled?`
12+
Promises are resolved asynchronously: parents are guaranteed to be resolved before their children.
13+
`then` takes two parameters: an optional block to be executed upon parent resolution and an optional callable
14+
to be executed upon parent failure.
15+
The result of each promise is passed to each of its children upon resolution.
16+
When a promise is rejected all its children will receive the reason.
17+
18+
Promises have four possible states: *unscheduled*, *pending*, *rejected*, and *fulfilled*.
19+
Promise created using `.new` will be *unscheduled*, to schedule it `execute` method must be called and the
20+
Promise with all its children will be set to *pending*.
21+
When a promise is *pending* will remain in that state until processing is complete.
22+
A completed promise is either *rejected*, indicating that an exception was thrown during processing, or *fulfilled*, indicating it succeeded.
23+
If a promise is *fulfilled* its `value` will be updated to reflect the result of the operation.
24+
If *rejected* the `reason` will be updated with a reference to the thrown exception.
25+
The predicate methods `unscheduled?`, `pending?`, `rejected`, and `fulfilled?`
2126
can be called at any time to obtain the state of the promise, as can the `state` method, which returns a symbol.
27+
Promise created using `.execute` will be *pending*, a Promise created using `.fulfill(value)` will be *fulfilled*
28+
with the passed value and a Promise created using `.reject(reason)` will be *rejected*.
2229

2330
Retrieving the value of a promise is done through the `value` (alias: `deref`) method. Obtaining the value of
2431
a promise is a potentially blocking operation. When a promise is *rejected* a call to `value` will return `nil`
@@ -41,40 +48,47 @@ require 'concurrent'
4148
Then create one
4249

4350
```ruby
44-
p = Promise.new("Jerry", "D'Antonio") do |first, last|
45-
"#{last}, #{first}"
51+
p = Promise.execute do
52+
# do something
53+
42
4654
end
4755
```
4856

4957
Promises can be chained using the `then` method. The `then` method
50-
accepts a block but no arguments. The result of the each promise is
51-
passed as the block argument to chained promises
58+
accepts a block, to be executed on fulfillment, and a callable argument to be executed on rejection.
59+
The result of the each promise is passed as the block argument to chained promises.
5260

5361
```ruby
54-
p = Concurrent::Promise.new(10){|x| x * 2}.then{|result| result - 10 }
62+
p = Concurrent::Promise.new{10}.then{|x| x * 2}.then{|result| result - 10 }
5563
```
5664

5765
And so on, and so on, and so on...
5866

5967
```ruby
60-
p = Concurrent::Promise.new(10){|x| x * 2}.
68+
p = Concurrent::Promise.fulfill(20).
6169
then{|result| result - 10 }.
6270
then{|result| result * 3 }.
6371
then{|result| result % 5 }
6472
```
6573

66-
Promises are executed asynchronously so a newly-created promise *should* always be in the pending state
74+
Newly created promise state depends on the parent's one:
75+
- if parent is *unscheduled* the child will be *unscheduled*
76+
- if parent is *pending* the child will be *pending*
77+
- if parent is *fulfilled* the child will be *pending*
78+
- if parent is *rejected* the child will be *pending*
79+
Promises are executed asynchronously so a newly-created promise could be in a different state right after
80+
`then` returns, for example a child of a *rejected* parent could be already *fulfilled* or *rejected*
6781

6882
```ruby
69-
p = Concurrent::Promise.new{ "Hello, world!" }
83+
p = Concurrent::Promise.execute{ "Hello, world!" }
7084
p.state #=> :pending
7185
p.pending? #=> true
7286
```
7387

7488
Wait a little bit, and the promise will resolve and provide a value
7589

7690
```ruby
77-
p = Concurrent::Promise.new{ "Hello, world!" }
91+
p = Concurrent::Promise.execute{ "Hello, world!" }
7892
sleep(0.1)
7993

8094
p.state #=> :fulfilled
@@ -86,7 +100,7 @@ If an exception occurs, the promise will be rejected and will provide
86100
a reason for the rejection
87101

88102
```ruby
89-
p = Concurrent::Promise.new{ raise StandardError.new("Here comes the Boom!") }
103+
p = Concurrent::Promise.execute{ raise StandardError.new("Here comes the Boom!") }
90104
sleep(0.1)
91105

92106
p.state #=> :rejected
@@ -96,96 +110,34 @@ p.reason #=> "#<StandardError: Here comes the Boom!>"
96110

97111
### Rejection
98112

99-
Much like the economy, rejection exhibits a trickle-down effect. When
100-
a promise is rejected all its children will be rejected
113+
When a promise is rejected all its children will be receive the reason as the rejection callable parameter.
101114

102115
```ruby
103-
p = [ Concurrent::Promise.new{ Thread.pass; raise StandardError } ]
116+
p = [ Concurrent::Promise.execute{ Thread.pass; raise StandardError } ]
104117

105-
10.times{|i| p << p.first.then{ i } }
106-
sleep(0.1)
107-
108-
p.length #=> 11
109-
p.first.state #=> :rejected
110-
p.last.state #=> :rejected
111-
```
118+
c1 = p.then(Proc.new{ |reason| 42 })
119+
c2 = p.then(Proc.new{ |reason| raise 'Boom!' })
112120

113-
Once a promise is rejected it will not accept any children. Calls
114-
to `then` will continually return `self`
115121

116-
```ruby
117-
p = Concurrent::Promise.new{ raise StandardError }
118122
sleep(0.1)
119123

120-
p.object_id #=> 32960556
121-
p.then{}.object_id #=> 32960556
122-
p.then{}.object_id #=> 32960556
123-
```
124-
125-
### Error Handling
126-
127-
Promises support error handling callbacks is a style mimicing Ruby's
128-
own exception handling mechanism, namely `rescue`
129-
130-
```ruby
131-
Concurrent::Promise.new{ "dangerous operation..." }.rescue{|ex| puts "Bam!" }
132-
133-
# -or- (for the Java/C# crowd)
134-
Concurrent::Promise.new{ "dangerous operation..." }.catch{|ex| puts "Boom!" }
135-
136-
# -or- (for the hipsters)
137-
Concurrent::Promise.new{ "dangerous operation..." }.on_error{|ex| puts "Pow!" }
138-
```
139-
140-
As with Ruby's `rescue` mechanism, a promise's `rescue` method can
141-
accept an optional Exception class argument (defaults to `Exception`
142-
when not specified)
143-
144-
```ruby
145-
Concurrent::Promise.new{ "dangerous operation..." }.rescue(ArgumentError){|ex| puts "Bam!" }
124+
c1.state #=> :fulfilled
125+
c2.state #=> :rejected
146126
```
147127

148-
Calls to `rescue` can also be chained
149-
150-
```ruby
151-
Concurrent::Promise.new{ "dangerous operation..." }.
152-
rescue(ArgumentError){|ex| puts "Bam!" }.
153-
rescue(NoMethodError){|ex| puts "Boom!" }.
154-
rescue(StandardError){|ex| puts "Pow!" }
155-
```
156-
157-
When there are multiple `rescue` handlers the first one to match the thrown
158-
exception will be triggered
159-
160-
```ruby
161-
Concurrent::Promise.new{ raise NoMethodError }.
162-
rescue(ArgumentError){|ex| puts "Bam!" }.
163-
rescue(NoMethodError){|ex| puts "Boom!" }.
164-
rescue(StandardError){|ex| puts "Pow!" }
165-
166-
sleep(0.1)
128+
Once a promise is rejected it will continue to accept children that will receive immediately
129+
rejection (they will be executed asynchronously)
167130

168-
#=> Boom!
169-
```
131+
### Aliases
170132

171-
Trickle-down rejection also applies to rescue handlers. When a promise is rejected,
172-
for any reason, its rescue handlers will be triggered. Rejection of the parent counts.
133+
`then` method is the most generic one: it accepts a block to be executed upon parent fulfillment
134+
and a callable to be executed upon parent rejection. At least one of them should be passed.
135+
Default block is `{ |result| result }` that fulfills the child with the parent value.
136+
Default callable is `{ |reason| raise reason }` that rejects the child with the parent reason.
173137

174-
```ruby
175-
Concurrent::Promise.new{ Thread.pass; raise StandardError }.
176-
then{ true }.rescue{ puts 'Boom!' }.
177-
then{ true }.rescue{ puts 'Boom!' }.
178-
then{ true }.rescue{ puts 'Boom!' }.
179-
then{ true }.rescue{ puts 'Boom!' }.
180-
then{ true }.rescue{ puts 'Boom!' }
181-
sleep(0.1)
182-
183-
#=> Boom!
184-
#=> Boom!
185-
#=> Boom!
186-
#=> Boom!
187-
#=> Boom!
188-
```
138+
`on_success { |result| ... }` is the same as `then {|result| ... }`
139+
`rescue { |reason| ... }` is the same as `then(Proc.new { |reason| ... } )`
140+
`rescue` is aliased by `catch` and `on_error`
189141

190142
## Copyright
191143

spec/concurrent/promise_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ module Concurrent
307307
expected.should eq 20
308308
end
309309

310+
it 'uses result as fulfillment value when a promise has no block' do
311+
p = Promise.new{ 20 }.then(Proc.new{}).execute
312+
sleep(0.1)
313+
p.value.should eq 20
314+
end
315+
310316
it 'can manage long chain' do
311317
root = Promise.new { 20 }
312318
p1 = root.then { |b| b * 3 }
@@ -345,6 +351,13 @@ module Concurrent
345351
p.should be_rejected
346352
end
347353

354+
it 'uses reason as rejection reason when a promise has no rescue callable' do
355+
p = Promise.new{ raise ArgumentError }.then { |val| val }.execute
356+
sleep(0.1)
357+
p.should be_rejected
358+
p.reason.should be_a ArgumentError
359+
end
360+
348361
end
349362

350363
context 'aliases' do

0 commit comments

Comments
 (0)