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 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