Skip to content

Commit 29cd69a

Browse files
committed
Merge pull request #198 from ruby-concurrency/promise-all
Initial implementation of Promises.all? and Promise.any? class methods.
2 parents 67d6631 + 4fb4b7d commit 29cd69a

File tree

3 files changed

+336
-124
lines changed

3 files changed

+336
-124
lines changed

doc/promise.md

Lines changed: 0 additions & 123 deletions
This file was deleted.

lib/concurrent/promise.rb

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,167 @@
55

66
module Concurrent
77

8-
# {include:file:doc/promise.md}
8+
PromiseExecutionError = Class.new(StandardError)
9+
10+
# Promises are inspired by the JavaScript [Promises/A](http://wiki.commonjs.org/wiki/Promises/A)
11+
# and [Promises/A+](http://promises-aplus.github.io/promises-spec/) specifications.
12+
#
13+
# > A promise represents the eventual value returned from the single completion of an operation.
14+
#
15+
# Promises are similar to futures and share many of the same behaviours. Promises are far more
16+
# robust, however. Promises can be chained in a tree structure where each promise may have zero
17+
# or more children. Promises are chained using the `then` method. The result of a call to `then`
18+
# is always another promise. Promises are resolved asynchronously (with respect to the main thread)
19+
# but in a strict order: parents are guaranteed to be resolved before their children, children
20+
# before their younger siblings. The `then` method takes two parameters: an optional block to
21+
# be executed upon parent resolution and an optional callable to be executed upon parent failure.
22+
# The result of each promise is passed to each of its children upon resolution. When a promise
23+
# is rejected all its children will be summarily rejected and will receive the reason.
24+
#
25+
# Promises have four possible states: *unscheduled*, *pending*, *rejected*, and *fulfilled*. A
26+
# Promise created using `.new` will be *unscheduled*. It is scheduled by calling the `execute`
27+
# method. Upon execution the Promise and all its children will be set to *pending*. When a promise
28+
# is *pending* it will remain in that state until processing is complete. A completed Promise is
29+
# either *rejected*, indicating that an exception was thrown during processing, or *fulfilled*,
30+
# indicating it succeeded. If a Promise is *fulfilled* its `value` will be updated to reflect
31+
# the result of the operation. If *rejected* the `reason` will be updated with a reference to
32+
# the thrown exception. The predicate methods `unscheduled?`, `pending?`, `rejected?`, and
33+
# `fulfilled?` can be called at any time to obtain the state of the Promise, as can the `state`
34+
# method, which returns a symbol. A Promise created using `.execute` will be *pending*, a Promise
35+
# created using `.fulfill(value)` will be *fulfilled* with the given value and a Promise created
36+
# using `.reject(reason)` will be *rejected* with the given reason.
37+
#
38+
# Retrieving the value of a promise is done through the `value` (alias: `deref`) method. Obtaining
39+
# the value of a promise is a potentially blocking operation. When a promise is *rejected* a call
40+
# to `value` will return `nil` immediately. When a promise is *fulfilled* a call to `value` will
41+
# immediately return the current value. When a promise is *pending* a call to `value` will block
42+
# until the promise is either *rejected* or *fulfilled*. A *timeout* value can be passed to `value`
43+
# to limit how long the call will block. If `nil` the call will block indefinitely. If `0` the call
44+
# will not block. Any other integer or float value will indicate the maximum number of seconds to block.
45+
#
46+
# Promises run on the global thread pool.
47+
#
48+
# ### Examples
49+
#
50+
# Start by requiring promises
51+
#
52+
# ```ruby
53+
# require 'concurrent'
54+
# ```
55+
#
56+
# Then create one
57+
#
58+
# ```ruby
59+
# p = Concurrent::Promise.execute do
60+
# # do something
61+
# 42
62+
# end
63+
# ```
64+
#
65+
# Promises can be chained using the `then` method. The `then` method accepts a block, to be executed
66+
# on fulfillment, and a callable argument to be executed on rejection. The result of the each promise
67+
# is passed as the block argument to chained promises.
68+
#
69+
# ```ruby
70+
# p = Concurrent::Promise.new{10}.then{|x| x * 2}.then{|result| result - 10 }.execute
71+
# ```
72+
#
73+
# And so on, and so on, and so on...
74+
#
75+
# ```ruby
76+
# p = Concurrent::Promise.fulfill(20).
77+
# then{|result| result - 10 }.
78+
# then{|result| result * 3 }.
79+
# then{|result| result % 5 }.execute
80+
# ```
81+
#
82+
# The initial state of a newly created Promise depends on the state of its parent:
83+
# - if parent is *unscheduled* the child will be *unscheduled*
84+
# - if parent is *pending* the child will be *pending*
85+
# - if parent is *fulfilled* the child will be *pending*
86+
# - if parent is *rejected* the child will be *pending* (but will ultimately be *rejected*)
87+
#
88+
# Promises are executed asynchronously from the main thread. By the time a child Promise finishes
89+
# nitialization it may be in a different state that its parent (by the time a child is created its parent
90+
# may have completed execution and changed state). Despite being asynchronous, however, the order of
91+
# execution of Promise objects in a chain (or tree) is strictly defined.
92+
#
93+
# There are multiple ways to create and execute a new `Promise`. Both ways provide identical behavior:
94+
#
95+
# ```ruby
96+
# # create, operate, then execute
97+
# p1 = Concurrent::Promise.new{ "Hello World!" }
98+
# p1.state #=> :unscheduled
99+
# p1.execute
100+
#
101+
# # create and immediately execute
102+
# p2 = Concurrent::Promise.new{ "Hello World!" }.execute
103+
#
104+
# # execute during creation
105+
# p3 = Concurrent::Promise.execute{ "Hello World!" }
106+
# ```
107+
#
108+
# Once the `execute` method is called a `Promise` becomes `pending`:
109+
#
110+
# ```ruby
111+
# p = Concurrent::Promise.execute{ "Hello, world!" }
112+
# p.state #=> :pending
113+
# p.pending? #=> true
114+
# ```
115+
#
116+
# Wait a little bit, and the promise will resolve and provide a value:
117+
#
118+
# ```ruby
119+
# p = Concurrent::Promise.execute{ "Hello, world!" }
120+
# sleep(0.1)
121+
#
122+
# p.state #=> :fulfilled
123+
# p.fulfilled? #=> true
124+
# p.value #=> "Hello, world!"
125+
# ```
126+
#
127+
# If an exception occurs, the promise will be rejected and will provide
128+
# a reason for the rejection:
129+
#
130+
# ```ruby
131+
# p = Concurrent::Promise.execute{ raise StandardError.new("Here comes the Boom!") }
132+
# sleep(0.1)
133+
#
134+
# p.state #=> :rejected
135+
# p.rejected? #=> true
136+
# p.reason #=> "#<StandardError: Here comes the Boom!>"
137+
# ```
138+
#
139+
# #### Rejection
140+
#
141+
# When a promise is rejected all its children will be rejected and will receive the rejection `reason`
142+
# as the rejection callable parameter:
143+
#
144+
# ```ruby
145+
# p = [ Concurrent::Promise.execute{ Thread.pass; raise StandardError } ]
146+
#
147+
# c1 = p.then(Proc.new{ |reason| 42 })
148+
# c2 = p.then(Proc.new{ |reason| raise 'Boom!' })
149+
#
150+
# sleep(0.1)
151+
#
152+
# c1.state #=> :rejected
153+
# c2.state #=> :rejected
154+
# ```
155+
#
156+
# Once a promise is rejected it will continue to accept children that will receive immediately rejection
157+
# (they will be executed asynchronously).
158+
#
159+
# #### Aliases
160+
#
161+
# The `then` method is the most generic alias: it accepts a block to be executed upon parent fulfillment
162+
# and a callable to be executed upon parent rejection. At least one of them should be passed. The default
163+
# block is `{ |result| result }` that fulfills the child with the parent value. The default callable is
164+
# `{ |reason| raise reason }` that rejects the child with the parent reason.
165+
#
166+
# - `on_success { |result| ... }` is the same as `then {|result| ... }`
167+
# - `rescue { |reason| ... }` is the same as `then(Proc.new { |reason| ... } )`
168+
# - `rescue` is aliased by `catch` and `on_error`
9169
class Promise
10170
# TODO unify promise and future to single class, with dataflow
11171
include Obligation
@@ -168,8 +328,66 @@ def zip(*others)
168328
self.class.zip(self, *others)
169329
end
170330

331+
# Aggregates a collection of promises and executes the `then` condition
332+
# if all aggregated promises succeed. Executes the `rescue` handler with
333+
# a `Concurrent::PromiseExecutionError` if any of the aggregated promises
334+
# fail. Upon execution will execute any of the aggregate promises that
335+
# were not already executed.
336+
#
337+
# @!macro [attach] promise_self_aggregate
338+
#
339+
# The returned promise will not yet have been executed. Additional `#then`
340+
# and `#rescue` handlers may still be provided. Once the returned promise
341+
# is execute the aggregate promises will be also be executed (if they have
342+
# not been executed already). The results of the aggregate promises will
343+
# be checked upon completion. The necessary `#then` and `#rescue` blocks
344+
# on the aggregating promise will then be executed as appropriate. If the
345+
# `#rescue` handlers are executed the raises exception will be
346+
# `Concurrent::PromiseExecutionError`.
347+
#
348+
# @param [Array] promises Zero or more promises to aggregate
349+
# @return [Promise] an unscheduled (not executed) promise that aggregates
350+
# the promises given as arguments
351+
def self.all?(*promises)
352+
aggregate(:all?, *promises)
353+
end
354+
355+
# Aggregates a collection of promises and executes the `then` condition
356+
# if any aggregated promises succeed. Executes the `rescue` handler with
357+
# a `Concurrent::PromiseExecutionError` if any of the aggregated promises
358+
# fail. Upon execution will execute any of the aggregate promises that
359+
# were not already executed.
360+
#
361+
# @!macro promise_self_aggregate
362+
def self.any?(*promises)
363+
aggregate(:any?, *promises)
364+
end
365+
171366
protected
172367

368+
# Aggregate a collection of zero or more promises under a composite promise,
369+
# execute the aggregated promises and collect them into a standard Ruby array,
370+
# call the given Ruby `Ennnumerable` predicate (such as `any?`, `all?`, `none?`,
371+
# or `one?`) on the collection checking for the success or failure of each,
372+
# then executing the composite's `#then` handlers if the predicate returns
373+
# `true` or executing the composite's `#rescue` handlers if the predicate
374+
# returns false.
375+
#
376+
# @!macro promise_self_aggregate
377+
def self.aggregate(method, *promises)
378+
composite = Promise.new do
379+
completed = promises.collect do |promise|
380+
promise.execute if promise.unscheduled?
381+
promise.wait
382+
promise
383+
end
384+
unless completed.empty? || completed.send(method){|promise| promise.fulfilled? }
385+
raise PromiseExecutionError
386+
end
387+
end
388+
composite
389+
end
390+
173391
def set_pending
174392
mutex.synchronize do
175393
@state = :pending

0 commit comments

Comments
 (0)