celluloid-promise 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Run all RSpec tests"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => :spec
10
+ task :test => [:spec]
@@ -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,6 @@
1
+ module Celluloid
2
+ module Promise
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
6
+
@@ -0,0 +1,2 @@
1
+ require 'celluloid'
2
+ require 'celluloid-promise/q.rb'
@@ -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