celluloid-promise 1.0.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.
- checksums.yaml +15 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +59 -0
- data/Rakefile +10 -0
- data/lib/celluloid-promise/q.rb +348 -0
- data/lib/celluloid-promise/version.rb +6 -0
- data/lib/celluloid-promise.rb +2 -0
- data/spec/promise_spec.rb +517 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
M2JmNWExNzczMTEzMjJjMDg4NTY1ZTkzZGU0YTEwZTEzNDI4MzFjYQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NmNiMzJjYzlkMWFlZWFhZGIwMTNlYjY3YzFmNzFiZDE0N2NiODQxMA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZmU0OTUzMDQ3MzZlOTE1ZGU3ZjgxMjE1ZjFjYzU1MWQwNmUzOTkwZmY5NzI4
|
10
|
+
Y2ViMDcxZTE2ZTNiZjU5NzhlZTk4OTQwZmY1ZGE4MDhlZTA2ZGZiNjRhNmQw
|
11
|
+
MjllYzU4ZTUwODM2NmYxMWJmMTdkZjRlYzI4MjUwYTk4NGUyYTU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NTYyYjgxOGIwZGY4MDdhZjQ2YTM2YWIyNGY4NWYwN2YwNzAzZThjMWM1MzZm
|
14
|
+
NmQ0Y2FmZTNhZmNlN2I0ODQ1MDg0NTNhZGE0MjU1MjY4MjU0ZmU4ZjViMzRl
|
15
|
+
NTZjZDc0NWQxODdlYTMyYjdkOWM3NjYwM2EyODIzZTkwYTcyMzU=
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
h1. Celluloid Promise
|
2
|
+
|
3
|
+
!https://secure.travis-ci.org/cotag/celluloid-promise.png!
|
4
|
+
|
5
|
+
A promise / deferred implementation for Celluloid inspired by "AngularJS":http://docs.angularjs.org/api/ng.$q / "Kris Kowal's Q.":https://github.com/kriskowal/q
|
6
|
+
|
7
|
+
From the perspective of dealing with error handling, deferred and promise apis are to asynchronous programing what try, catch and throw keywords are to synchronous programming.
|
8
|
+
|
9
|
+
<pre><code class="ruby">
|
10
|
+
require 'rubygems'
|
11
|
+
require 'celluloid-promise'
|
12
|
+
|
13
|
+
def asyncGreet(name)
|
14
|
+
deferred = Celluloid::Q.defer
|
15
|
+
|
16
|
+
Thread.new do
|
17
|
+
sleep 10
|
18
|
+
deferred.resolve("Hello #{name}")
|
19
|
+
end
|
20
|
+
|
21
|
+
deferred.promise
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
asyncGreet('Robin Hood').then(proc { |greeting|
|
26
|
+
p "Success: #{greeting}"
|
27
|
+
}, proc { |reason|
|
28
|
+
p "Failed: #{reason}"
|
29
|
+
})
|
30
|
+
|
31
|
+
asyncGreet('The Dude').then do |greeting|
|
32
|
+
p "Jeff '#{greeting}' Lebowski"
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
</code></pre>
|
37
|
+
|
38
|
+
|
39
|
+
h2. Start using it now
|
40
|
+
|
41
|
+
# Read the "Documentation":http://rubydoc.info/gems/celluloid-promise/Celluloid/Promise
|
42
|
+
# Then @gem install celluloid-promise@
|
43
|
+
|
44
|
+
|
45
|
+
h2. Use case
|
46
|
+
|
47
|
+
Celluloid provides a toolkit for building concurrent applications however coordinating complex chains of concurrent events can be difficult.
|
48
|
+
Celluloid-Promise provides the following:
|
49
|
+
|
50
|
+
* allows you to track a mash up of events across Actors
|
51
|
+
* guarantees serial execution of callbacks
|
52
|
+
* callback chains are spread across multiple threads
|
53
|
+
** So multiple promise chains can be executed concurrently
|
54
|
+
|
55
|
+
Of course passing blocks around is a fairly "dangerous exercise":https://github.com/celluloid/celluloid/wiki/Blocks#proposal-for-fixing-blocks, so do the following and you'll be safe
|
56
|
+
|
57
|
+
* use Celluloid-Promise for flow control
|
58
|
+
* resolve and reject promises with locally scoped variables
|
59
|
+
* call Actor methods to change state or class variables
|
data/Rakefile
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
module Celluloid
|
2
|
+
module Promise
|
3
|
+
|
4
|
+
#
|
5
|
+
# Used to serialise the chaining of promises on a thread
|
6
|
+
# Allowing the promise to safely be called on any other thread
|
7
|
+
#
|
8
|
+
class Reactor
|
9
|
+
include ::Celluloid
|
10
|
+
|
11
|
+
def task
|
12
|
+
yield if block_given?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
# @abstract
|
19
|
+
class Promise
|
20
|
+
private_class_method :new
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
#
|
25
|
+
# A new promise instance is created when a deferred instance is created and can be
|
26
|
+
# retrieved by calling deferred.promise
|
27
|
+
#
|
28
|
+
class DeferredPromise < Promise
|
29
|
+
public_class_method :new
|
30
|
+
|
31
|
+
def initialize(defer, reactor)
|
32
|
+
raise ArgumentError unless defer.is_a?(Deferred)
|
33
|
+
super()
|
34
|
+
|
35
|
+
@defer = defer
|
36
|
+
@reactor = reactor
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# regardless of when the promise was or will be resolved / rejected, calls one of
|
41
|
+
# the success or error callbacks asynchronously as soon as the result is available.
|
42
|
+
# The callbacks are called with a single argument, the result or rejection reason.
|
43
|
+
#
|
44
|
+
# @param [Proc, Proc, &blk] callbacks success, error, success_block
|
45
|
+
# @return [Promise] Returns an unresolved promise for chaining
|
46
|
+
def then(callback = nil, errback = nil, &blk)
|
47
|
+
result = Q.defer
|
48
|
+
|
49
|
+
callback ||= blk
|
50
|
+
|
51
|
+
wrappedCallback = proc { |val|
|
52
|
+
begin
|
53
|
+
if callback.nil?
|
54
|
+
result.resolve(val)
|
55
|
+
else
|
56
|
+
result.resolve(callback.call(val))
|
57
|
+
end
|
58
|
+
rescue => e
|
59
|
+
#warn "\nUnhandled exception: #{e.message}\n#{e.backtrace.join("\n")}\n"
|
60
|
+
result.reject(e);
|
61
|
+
end
|
62
|
+
}
|
63
|
+
|
64
|
+
wrappedErrback = proc { |reason|
|
65
|
+
begin
|
66
|
+
if errback.nil?
|
67
|
+
result.resolve(ResolvedPromise.new(@reactor, reason, true))
|
68
|
+
else
|
69
|
+
result.resolve(errback.call(reason))
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
#warn "Unhandled exception: #{e.message}\n#{e.backtrace.join("\n")}\n"
|
73
|
+
result.reject(e);
|
74
|
+
end
|
75
|
+
}
|
76
|
+
|
77
|
+
#
|
78
|
+
# Schedule as we are touching shared state
|
79
|
+
# Everything else is locally scoped
|
80
|
+
#
|
81
|
+
@reactor.task! do
|
82
|
+
pending_array = pending
|
83
|
+
|
84
|
+
if pending_array.nil?
|
85
|
+
value.then(wrappedCallback, wrappedErrback)
|
86
|
+
else
|
87
|
+
pending_array << [wrappedCallback, wrappedErrback]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
result.promise
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
|
98
|
+
def pending
|
99
|
+
@defer.instance_eval { @pending }
|
100
|
+
end
|
101
|
+
|
102
|
+
def value
|
103
|
+
@defer.instance_eval { @value }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
class ResolvedPromise < Promise
|
109
|
+
public_class_method :new
|
110
|
+
|
111
|
+
def initialize(reactor, response, error = false)
|
112
|
+
raise ArgumentError if error && response.is_a?(Promise)
|
113
|
+
super()
|
114
|
+
|
115
|
+
@error = error
|
116
|
+
@response = response
|
117
|
+
@reactor = reactor
|
118
|
+
end
|
119
|
+
|
120
|
+
def then(callback = nil, errback = nil, &blk)
|
121
|
+
result = Q.defer
|
122
|
+
|
123
|
+
callback ||= blk
|
124
|
+
|
125
|
+
@reactor.task! do
|
126
|
+
if @error
|
127
|
+
if errback.nil?
|
128
|
+
result.resolve(ResolvedPromise.new(@reactor, @response, true))
|
129
|
+
else
|
130
|
+
result.resolve(errback.call(@response))
|
131
|
+
end
|
132
|
+
else
|
133
|
+
if callback.nil?
|
134
|
+
result.resolve(@response)
|
135
|
+
else
|
136
|
+
result.resolve(callback.call(@response))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
result.promise
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
#
|
148
|
+
# The purpose of the deferred object is to expose the associated Promise instance as well
|
149
|
+
# as APIs that can be used for signalling the successful or unsuccessful completion of a task.
|
150
|
+
#
|
151
|
+
class Deferred
|
152
|
+
|
153
|
+
def initialize(reactor)
|
154
|
+
super()
|
155
|
+
|
156
|
+
@reactor = reactor
|
157
|
+
@pending = []
|
158
|
+
@value = nil
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# resolves the derived promise with the value. If the value is a rejection constructed via
|
163
|
+
# Q.reject, the promise will be rejected instead.
|
164
|
+
#
|
165
|
+
# @param [Object] val constant, message or an object representing the result.
|
166
|
+
def resolve(val = nil)
|
167
|
+
@reactor.task! do
|
168
|
+
if not @pending.nil?
|
169
|
+
callbacks = @pending
|
170
|
+
@pending = nil
|
171
|
+
@value = ref(val)
|
172
|
+
|
173
|
+
if callbacks.length > 0
|
174
|
+
callbacks.each do |callback|
|
175
|
+
@value.then(callback[0], callback[1])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# rejects the derived promise with the reason. This is equivalent to resolving it with a rejection
|
184
|
+
# constructed via Q.reject.
|
185
|
+
#
|
186
|
+
# @param [Object] reason constant, message, exception or an object representing the rejection reason.
|
187
|
+
def reject(reason = nil)
|
188
|
+
resolve(ResolvedPromise.new(@reactor, reason, true))
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Creates a promise object associated with this deferred
|
193
|
+
#
|
194
|
+
def promise
|
195
|
+
DeferredPromise.new( self, @reactor )
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
|
202
|
+
def ref(value)
|
203
|
+
return value if value.is_a?(Promise)
|
204
|
+
return ResolvedPromise.new( @reactor, value ) # A resolved success promise
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
class Coordinator
|
210
|
+
include ::Celluloid
|
211
|
+
|
212
|
+
|
213
|
+
def initialize
|
214
|
+
@reactors = []
|
215
|
+
@current = -1 # So we pick 0 first
|
216
|
+
threads = ::Celluloid.cores
|
217
|
+
threads += 1 if threads == 1
|
218
|
+
threads.times { @reactors << Reactor.new_link } # Have a thread for each core and link promises to each thread for serialisation
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
#
|
223
|
+
# Creates a Deferred object which represents a task which will finish in the future.
|
224
|
+
#
|
225
|
+
# @return [Deferred] Returns a new instance of Deferred
|
226
|
+
def defer
|
227
|
+
return Deferred.new(next_reactor)
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
#
|
232
|
+
# Creates a promise that is resolved as rejected with the specified reason. This api should be
|
233
|
+
# used to forward rejection in a chain of promises. If you are dealing with the last promise in
|
234
|
+
# a promise chain, you don't need to worry about it.
|
235
|
+
#
|
236
|
+
# When comparing deferreds/promises to the familiar behaviour of try/catch/throw, think of
|
237
|
+
# reject as the raise keyword in Ruby. This also means that if you "catch" an error via
|
238
|
+
# a promise error callback and you want to forward the error to the promise derived from the
|
239
|
+
# current promise, you have to "rethrow" the error by returning a rejection constructed via
|
240
|
+
# reject.
|
241
|
+
#
|
242
|
+
# @example handling rejections
|
243
|
+
#
|
244
|
+
# #!/usr/bin/env ruby
|
245
|
+
#
|
246
|
+
# require 'rubygems' # or use Bundler.setup
|
247
|
+
# require 'celluloid-promise'
|
248
|
+
#
|
249
|
+
# promiseB = promiseA.then(lambda {|result|
|
250
|
+
# # success: do something and resolve promiseB with the old or a new result
|
251
|
+
# return result
|
252
|
+
# }, lambda {|reason|
|
253
|
+
# # error: handle the error if possible and resolve promiseB with newPromiseOrValue,
|
254
|
+
# # otherwise forward the rejection to promiseB
|
255
|
+
# if canHandle(reason)
|
256
|
+
# # handle the error and recover
|
257
|
+
# return newPromiseOrValue
|
258
|
+
# end
|
259
|
+
# return Q.reject(reason)
|
260
|
+
# })
|
261
|
+
#
|
262
|
+
# @param [Object] reason constant, message, exception or an object representing the rejection reason.
|
263
|
+
# @return [Promise] Returns a promise that was already resolved as rejected with the reason
|
264
|
+
def reject(reason = nil)
|
265
|
+
return ResolvedPromise.new(next_reactor, reason, true) # A resolved failed promise
|
266
|
+
end
|
267
|
+
|
268
|
+
|
269
|
+
#
|
270
|
+
# Combines multiple promises into a single promise that is resolved when all of the input
|
271
|
+
# promises are resolved.
|
272
|
+
#
|
273
|
+
# @param [*Promise] Promises a number of promises that will be combined into a single promise
|
274
|
+
# @return [Promise] Returns a single promise that will be resolved with an array of values,
|
275
|
+
# each value corresponding to the promise at the same index in the `promises` array. If any of
|
276
|
+
# the promises is resolved with a rejection, this resulting promise will be resolved with the
|
277
|
+
# same rejection.
|
278
|
+
def all(*promises)
|
279
|
+
reactor = next_reactor
|
280
|
+
deferred = Deferred.new(reactor)
|
281
|
+
counter = promises.length
|
282
|
+
results = []
|
283
|
+
|
284
|
+
if counter > 0
|
285
|
+
promises.each_index do |index|
|
286
|
+
ref(promises[index], reactor).then(proc {|result|
|
287
|
+
if results[index].nil?
|
288
|
+
results[index] = result
|
289
|
+
counter -= 1
|
290
|
+
deferred.resolve(results) if counter <= 0
|
291
|
+
end
|
292
|
+
result
|
293
|
+
}, proc {|reason|
|
294
|
+
if results[index].nil?
|
295
|
+
deferred.reject(reason)
|
296
|
+
end
|
297
|
+
reason
|
298
|
+
})
|
299
|
+
end
|
300
|
+
else
|
301
|
+
deferred.resolve(results)
|
302
|
+
end
|
303
|
+
|
304
|
+
return deferred.promise
|
305
|
+
end
|
306
|
+
|
307
|
+
|
308
|
+
private
|
309
|
+
|
310
|
+
|
311
|
+
def ref(value, reactor)
|
312
|
+
return value if value.is_a?(Promise)
|
313
|
+
return ResolvedPromise.new( reactor, value ) # A resolved success promise
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
#
|
318
|
+
# Promises are placed on reactor threads in a round robin
|
319
|
+
# I would have used pool however Celluloid::IO allows us to run
|
320
|
+
# multiple concurrent promise chains on each reactor, pool can't.
|
321
|
+
#
|
322
|
+
def next_reactor
|
323
|
+
@current = @current >= (@reactors.length - 1) ? 0 : @current + 1
|
324
|
+
selected = @reactors[@current]
|
325
|
+
selected != Celluloid::Actor.current ? selected : next_reactor
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
#
|
331
|
+
# This is the primary interface for creating promises
|
332
|
+
# The coordinator selects the reactor for the current promise chain
|
333
|
+
#
|
334
|
+
Actor[:Q] = Promise::Coordinator.new
|
335
|
+
module Q
|
336
|
+
def self.defer
|
337
|
+
Actor[:Q].defer
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.reject(reason = nil)
|
341
|
+
Actor[:Q].reject(reason)
|
342
|
+
end
|
343
|
+
|
344
|
+
def self.all(*promises)
|
345
|
+
Actor[:Q].all(*promises)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
@@ -0,0 +1,517 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'celluloid-promise'
|
5
|
+
require 'atomic'
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
describe Celluloid::Q do
|
10
|
+
|
11
|
+
before :all do
|
12
|
+
@defer = Celluloid::Promise::Reactor.pool
|
13
|
+
end
|
14
|
+
|
15
|
+
before :each do
|
16
|
+
@deferred = Celluloid::Q.defer
|
17
|
+
@promise = @deferred.promise
|
18
|
+
@log = []
|
19
|
+
@finish = proc {
|
20
|
+
while(@mutex.locked?); end
|
21
|
+
@mutex.synchronize {
|
22
|
+
@resource.signal
|
23
|
+
}
|
24
|
+
}
|
25
|
+
@default_fail = proc { |reason|
|
26
|
+
fail(reason)
|
27
|
+
@finish.call
|
28
|
+
}
|
29
|
+
@mutex = Mutex.new
|
30
|
+
@resource = ConditionVariable.new
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
describe Celluloid::Promise::Coordinator do
|
36
|
+
|
37
|
+
|
38
|
+
describe 'resolve' do
|
39
|
+
|
40
|
+
|
41
|
+
it "should call the callback in the next turn" do
|
42
|
+
@mutex.synchronize {
|
43
|
+
@promise.then(proc {|result|
|
44
|
+
@log << result
|
45
|
+
@finish.call
|
46
|
+
}, @default_fail)
|
47
|
+
|
48
|
+
@deferred.resolve(:foo)
|
49
|
+
@resource.wait(@mutex)
|
50
|
+
}
|
51
|
+
|
52
|
+
@log.should == [:foo]
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
it "should fulfill success callbacks in the registration order" do
|
57
|
+
@mutex.synchronize {
|
58
|
+
@promise.then(proc {|result|
|
59
|
+
@log << :first
|
60
|
+
}, @default_fail)
|
61
|
+
|
62
|
+
@promise.then(proc {|result|
|
63
|
+
@log << :second
|
64
|
+
@finish.call
|
65
|
+
}, @default_fail)
|
66
|
+
|
67
|
+
@deferred.resolve(:foo)
|
68
|
+
@resource.wait(@mutex)
|
69
|
+
}
|
70
|
+
|
71
|
+
@log.should == [:first, :second]
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
it "should do nothing if a promise was previously resolved" do
|
76
|
+
@mutex.synchronize {
|
77
|
+
@promise.then(proc {|result|
|
78
|
+
@log << result
|
79
|
+
@log.should == [:foo]
|
80
|
+
@deferred.resolve(:bar)
|
81
|
+
@finish.call
|
82
|
+
}, @default_fail)
|
83
|
+
|
84
|
+
@deferred.resolve(:foo)
|
85
|
+
@deferred.reject(:baz)
|
86
|
+
@resource.wait(@mutex)
|
87
|
+
}
|
88
|
+
|
89
|
+
@log.should == [:foo]
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
it "should allow deferred resolution with a new promise" do
|
94
|
+
deferred2 = Celluloid::Q.defer
|
95
|
+
@mutex.synchronize {
|
96
|
+
@promise.then(proc {|result|
|
97
|
+
result.should == :foo
|
98
|
+
@finish.call
|
99
|
+
}, @default_fail)
|
100
|
+
|
101
|
+
@deferred.resolve(deferred2.promise)
|
102
|
+
deferred2.resolve(:foo)
|
103
|
+
|
104
|
+
@resource.wait(@mutex)
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
it "should not break if a callbacks registers another callback" do
|
110
|
+
@mutex.synchronize {
|
111
|
+
@promise.then(proc {|result|
|
112
|
+
@log << :outer
|
113
|
+
@promise.then(proc {|result|
|
114
|
+
@log << :inner
|
115
|
+
@finish.call
|
116
|
+
}, @default_fail)
|
117
|
+
}, @default_fail)
|
118
|
+
|
119
|
+
@deferred.resolve(:foo)
|
120
|
+
|
121
|
+
@resource.wait(@mutex)
|
122
|
+
@log.should == [:outer, :inner]
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
it "can modify the result of a promise before returning" do
|
128
|
+
@mutex.synchronize {
|
129
|
+
proc { |name|
|
130
|
+
@defer.task! {
|
131
|
+
@deferred.resolve("Hello #{name}")
|
132
|
+
}
|
133
|
+
@promise.then(proc {|result|
|
134
|
+
result.should == 'Hello Robin Hood'
|
135
|
+
result += "?"
|
136
|
+
result
|
137
|
+
})
|
138
|
+
}.call('Robin Hood').then(proc { |greeting|
|
139
|
+
greeting.should == 'Hello Robin Hood?'
|
140
|
+
@finish.call
|
141
|
+
}, @default_fail)
|
142
|
+
|
143
|
+
@resource.wait(@mutex)
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
describe 'reject' do
|
152
|
+
it "should reject the promise and execute all error callbacks" do
|
153
|
+
@mutex.synchronize {
|
154
|
+
@promise.then(@default_fail, proc {|result|
|
155
|
+
@log << :first
|
156
|
+
})
|
157
|
+
@promise.then(@default_fail, proc {|result|
|
158
|
+
@log << :second
|
159
|
+
@finish.call
|
160
|
+
})
|
161
|
+
|
162
|
+
@deferred.reject(:foo)
|
163
|
+
|
164
|
+
@resource.wait(@mutex)
|
165
|
+
}
|
166
|
+
|
167
|
+
@log.should == [:first, :second]
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
it "should do nothing if a promise was previously rejected" do
|
172
|
+
@mutex.synchronize {
|
173
|
+
@promise.then(@default_fail, proc {|result|
|
174
|
+
@log << result
|
175
|
+
@log.should == [:baz]
|
176
|
+
@deferred.resolve(:bar)
|
177
|
+
|
178
|
+
@finish.call
|
179
|
+
})
|
180
|
+
|
181
|
+
@deferred.reject(:baz)
|
182
|
+
@deferred.resolve(:foo)
|
183
|
+
|
184
|
+
|
185
|
+
@resource.wait(@mutex)
|
186
|
+
}
|
187
|
+
|
188
|
+
@log.should == [:baz]
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
it "should not defer rejection with a new promise" do
|
193
|
+
deferred2 = Celluloid::Q.defer
|
194
|
+
@promise.then(@default_fail, @default_fail)
|
195
|
+
begin
|
196
|
+
@deferred.reject(deferred2.promise)
|
197
|
+
rescue => e
|
198
|
+
e.is_a?(ArgumentError).should == true
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
it "should package a string into a rejected promise" do
|
204
|
+
@mutex.synchronize {
|
205
|
+
rejectedPromise = Celluloid::Q.reject('not gonna happen')
|
206
|
+
|
207
|
+
@promise.then(nil, proc {|reason|
|
208
|
+
@log << reason
|
209
|
+
@finish.call
|
210
|
+
})
|
211
|
+
|
212
|
+
@deferred.resolve(rejectedPromise)
|
213
|
+
|
214
|
+
@resource.wait(@mutex)
|
215
|
+
@log.should == ['not gonna happen']
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
it "should return a promise that forwards callbacks if the callbacks are missing" do
|
221
|
+
@mutex.synchronize {
|
222
|
+
rejectedPromise = Celluloid::Q.reject('not gonna happen')
|
223
|
+
|
224
|
+
@promise.then(nil, proc {|reason|
|
225
|
+
@log << reason
|
226
|
+
@finish.call
|
227
|
+
})
|
228
|
+
|
229
|
+
@deferred.resolve(rejectedPromise.then())
|
230
|
+
|
231
|
+
@resource.wait(@mutex)
|
232
|
+
@log.should == ['not gonna happen']
|
233
|
+
}
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
describe 'all' do
|
240
|
+
|
241
|
+
it "should resolve all of nothing" do
|
242
|
+
@mutex.synchronize {
|
243
|
+
Celluloid::Q.all.then(proc {|result|
|
244
|
+
@log << result
|
245
|
+
@finish.call
|
246
|
+
}, @default_fail)
|
247
|
+
|
248
|
+
|
249
|
+
@resource.wait(@mutex)
|
250
|
+
}
|
251
|
+
|
252
|
+
@log.should == [[]]
|
253
|
+
end
|
254
|
+
|
255
|
+
it "should take an array of promises and return a promise for an array of results" do
|
256
|
+
@mutex.synchronize {
|
257
|
+
deferred1 = Celluloid::Q.defer
|
258
|
+
deferred2 = Celluloid::Q.defer
|
259
|
+
|
260
|
+
Celluloid::Q.all(@promise, deferred1.promise, deferred2.promise).then(proc {|result|
|
261
|
+
result.should == [:foo, :bar, :baz]
|
262
|
+
@finish.call
|
263
|
+
}, @default_fail)
|
264
|
+
|
265
|
+
@defer.task! { @deferred.resolve(:foo) }
|
266
|
+
@defer.task! { deferred2.resolve(:baz) }
|
267
|
+
@defer.task! { deferred1.resolve(:bar) }
|
268
|
+
|
269
|
+
@resource.wait(@mutex)
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
it "should reject the derived promise if at least one of the promises in the array is rejected" do
|
275
|
+
@mutex.synchronize {
|
276
|
+
deferred1 = Celluloid::Q.defer
|
277
|
+
deferred2 = Celluloid::Q.defer
|
278
|
+
|
279
|
+
Celluloid::Q.all(@promise, deferred1.promise, deferred2.promise).then(@default_fail, proc {|reason|
|
280
|
+
reason.should == :baz
|
281
|
+
@finish.call
|
282
|
+
})
|
283
|
+
|
284
|
+
@defer.task! { @deferred.resolve(:foo) }
|
285
|
+
@defer.task! { deferred2.reject(:baz) }
|
286
|
+
|
287
|
+
@resource.wait(@mutex)
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
293
|
+
end
|
294
|
+
|
295
|
+
|
296
|
+
describe Celluloid::Promise do
|
297
|
+
|
298
|
+
describe 'then' do
|
299
|
+
|
300
|
+
it "should allow registration of a success callback without an errback and resolve" do
|
301
|
+
@mutex.synchronize {
|
302
|
+
@promise.then(proc {|result|
|
303
|
+
@log << result
|
304
|
+
})
|
305
|
+
@promise.then(proc {
|
306
|
+
@finish.call
|
307
|
+
}, @default_fail)
|
308
|
+
|
309
|
+
@deferred.resolve(:foo)
|
310
|
+
|
311
|
+
@resource.wait(@mutex)
|
312
|
+
@log.should == [:foo]
|
313
|
+
}
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
it "should allow registration of a success callback without an errback and reject" do
|
318
|
+
@mutex.synchronize {
|
319
|
+
@promise.then(proc {|result|
|
320
|
+
@log << result
|
321
|
+
})
|
322
|
+
@promise.then(@default_fail, proc {
|
323
|
+
@finish.call
|
324
|
+
})
|
325
|
+
|
326
|
+
@deferred.reject(:foo)
|
327
|
+
|
328
|
+
@resource.wait(@mutex)
|
329
|
+
@log.should == []
|
330
|
+
}
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
it "should allow registration of an errback without a success callback and reject" do
|
335
|
+
@mutex.synchronize {
|
336
|
+
@promise.then(nil, proc {|reason|
|
337
|
+
@log << reason
|
338
|
+
})
|
339
|
+
@promise.then(@default_fail, proc {
|
340
|
+
@finish.call
|
341
|
+
})
|
342
|
+
|
343
|
+
@deferred.reject(:foo)
|
344
|
+
|
345
|
+
@resource.wait(@mutex)
|
346
|
+
@log.should == [:foo]
|
347
|
+
}
|
348
|
+
end
|
349
|
+
|
350
|
+
|
351
|
+
it "should allow registration of an errback without a success callback and resolve" do
|
352
|
+
@mutex.synchronize {
|
353
|
+
@promise.then(nil, proc {|reason|
|
354
|
+
@log << reason
|
355
|
+
})
|
356
|
+
@promise.then(proc {
|
357
|
+
@finish.call
|
358
|
+
}, @default_fail)
|
359
|
+
|
360
|
+
@deferred.resolve(:foo)
|
361
|
+
|
362
|
+
@resource.wait(@mutex)
|
363
|
+
@log.should == []
|
364
|
+
}
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
it "should resolve all callbacks with the original value" do
|
369
|
+
@mutex.synchronize {
|
370
|
+
@promise.then(proc {|result|
|
371
|
+
@log << result
|
372
|
+
:alt1
|
373
|
+
}, @default_fail)
|
374
|
+
@promise.then(proc {|result|
|
375
|
+
@log << result
|
376
|
+
'ERROR'
|
377
|
+
}, @default_fail)
|
378
|
+
@promise.then(proc {|result|
|
379
|
+
@log << result
|
380
|
+
EM::Q.reject('some reason')
|
381
|
+
}, @default_fail)
|
382
|
+
@promise.then(proc {|result|
|
383
|
+
@log << result
|
384
|
+
:alt2
|
385
|
+
}, @default_fail)
|
386
|
+
@promise.then(proc {
|
387
|
+
@finish.call
|
388
|
+
}, @default_fail)
|
389
|
+
|
390
|
+
@deferred.resolve(:foo)
|
391
|
+
|
392
|
+
@resource.wait(@mutex)
|
393
|
+
@log.should == [:foo, :foo, :foo, :foo]
|
394
|
+
}
|
395
|
+
end
|
396
|
+
|
397
|
+
|
398
|
+
it "should reject all callbacks with the original reason" do
|
399
|
+
@mutex.synchronize {
|
400
|
+
@promise.then(@default_fail, proc {|result|
|
401
|
+
@log << result
|
402
|
+
:alt1
|
403
|
+
})
|
404
|
+
@promise.then(@default_fail, proc {|result|
|
405
|
+
@log << result
|
406
|
+
'ERROR'
|
407
|
+
})
|
408
|
+
@promise.then(@default_fail, proc {|result|
|
409
|
+
@log << result
|
410
|
+
EM::Q.reject('some reason')
|
411
|
+
})
|
412
|
+
@promise.then(@default_fail, proc {|result|
|
413
|
+
@log << result
|
414
|
+
:alt2
|
415
|
+
})
|
416
|
+
@promise.then(@default_fail, proc {|result|
|
417
|
+
@finish.call
|
418
|
+
})
|
419
|
+
|
420
|
+
@deferred.reject(:foo)
|
421
|
+
|
422
|
+
@resource.wait(@mutex)
|
423
|
+
@log.should == [:foo, :foo, :foo, :foo]
|
424
|
+
}
|
425
|
+
end
|
426
|
+
|
427
|
+
|
428
|
+
it "should propagate resolution and rejection between dependent promises" do
|
429
|
+
@mutex.synchronize {
|
430
|
+
@promise.then(proc {|result|
|
431
|
+
@log << result
|
432
|
+
:bar
|
433
|
+
}, @default_fail).then(proc {|result|
|
434
|
+
@log << result
|
435
|
+
raise 'baz'
|
436
|
+
}, @default_fail).then(@default_fail, proc {|result|
|
437
|
+
@log << result.message
|
438
|
+
raise 'bob'
|
439
|
+
}).then(@default_fail, proc {|result|
|
440
|
+
@log << result.message
|
441
|
+
:done
|
442
|
+
}).then(proc {|result|
|
443
|
+
@log << result
|
444
|
+
@finish.call
|
445
|
+
}, @default_fail)
|
446
|
+
|
447
|
+
@deferred.resolve(:foo)
|
448
|
+
|
449
|
+
@resource.wait(@mutex)
|
450
|
+
@log.should == [:foo, :bar, 'baz', 'bob', :done]
|
451
|
+
}
|
452
|
+
end
|
453
|
+
|
454
|
+
|
455
|
+
it "should call error callback even if promise is already rejected" do
|
456
|
+
@mutex.synchronize {
|
457
|
+
@deferred.reject(:foo)
|
458
|
+
|
459
|
+
@promise.then(nil, proc {|reason|
|
460
|
+
@log << reason
|
461
|
+
@finish.call
|
462
|
+
})
|
463
|
+
|
464
|
+
@resource.wait(@mutex)
|
465
|
+
@log.should == [:foo]
|
466
|
+
}
|
467
|
+
end
|
468
|
+
|
469
|
+
|
470
|
+
class BlockingActor
|
471
|
+
include ::Celluloid
|
472
|
+
|
473
|
+
def wait
|
474
|
+
sleep (10..20).to_a.sample
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
|
479
|
+
it "should not block when waiting for other promises to finish" do
|
480
|
+
@mutex.synchronize {
|
481
|
+
|
482
|
+
actor = BlockingActor.new
|
483
|
+
count = Atomic.new(0)
|
484
|
+
cores = ::Celluloid.cores
|
485
|
+
deferreds = cores * 2
|
486
|
+
deferred_store = []
|
487
|
+
|
488
|
+
args = [proc {|val|
|
489
|
+
actor.wait
|
490
|
+
count.update {|v| v + 1}
|
491
|
+
}, @default_fail]
|
492
|
+
|
493
|
+
|
494
|
+
deferreds.times {
|
495
|
+
defer = Celluloid::Q.defer
|
496
|
+
defer.promise.then(*args)
|
497
|
+
deferred_store << defer
|
498
|
+
defer.resolve()
|
499
|
+
}
|
500
|
+
|
501
|
+
@promise.then(proc {|result|
|
502
|
+
(!!(count.value < cores)).should == true
|
503
|
+
@finish.call
|
504
|
+
}, @default_fail)
|
505
|
+
|
506
|
+
|
507
|
+
@deferred.resolve()
|
508
|
+
@resource.wait(@mutex)
|
509
|
+
}
|
510
|
+
end
|
511
|
+
|
512
|
+
end
|
513
|
+
|
514
|
+
end
|
515
|
+
|
516
|
+
end
|
517
|
+
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: celluloid-promise
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen von Takach
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: celluloid
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: atomic
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Celluloid based multi-threaded promise implementation
|
56
|
+
email:
|
57
|
+
- steve@cotag.me
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- lib/celluloid-promise/q.rb
|
63
|
+
- lib/celluloid-promise/version.rb
|
64
|
+
- lib/celluloid-promise.rb
|
65
|
+
- MIT-LICENSE
|
66
|
+
- Rakefile
|
67
|
+
- README.textile
|
68
|
+
- spec/promise_spec.rb
|
69
|
+
homepage: https://github.com/cotag/celluloid-promise
|
70
|
+
licenses: []
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 2.0.0
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: Celluloid based multi-threaded promise implementation
|
92
|
+
test_files:
|
93
|
+
- spec/promise_spec.rb
|