deferrable_gratification 0.1.0 → 0.2.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.
data/README.markdown
CHANGED
@@ -1,64 +1,361 @@
|
|
1
1
|
# Deferrable Gratification #
|
2
2
|
|
3
|
-
|
3
|
+
Deferrable Gratification (DG) facilitates asynchronous programming in Ruby, by
|
4
|
+
helping create abstractions around complex operations built up from simpler
|
5
|
+
ones. It helps make asynchronous code less error-prone and easier to compose.
|
6
|
+
It also provides some enhancements to the
|
7
|
+
[`Deferrable`](http://eventmachine.rubyforge.org/EventMachine/Deferrable.html)
|
8
|
+
API.
|
4
9
|
|
5
|
-
|
6
|
-
compose, and thus easier to create higher-level abstractions around. It also
|
7
|
-
enhances the API offered by Ruby Deferrables to make them more pleasant to work
|
8
|
-
with.
|
10
|
+
## Motivation ##
|
9
11
|
|
10
|
-
|
12
|
+
Asynchronous programming, as supported in Ruby by
|
13
|
+
[EventMachine](http://rubyeventmachine.com/), offers the benefits of (limited)
|
14
|
+
concurrency without the complexity of threads. However, it requires a style of
|
15
|
+
code that fits awkwardly into Ruby's synchronous semantics.
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
|
17
|
+
A method that performs an asynchronous operation cannot simply return a result:
|
18
|
+
instead it must take a callback, which it calls with the eventual result of the
|
19
|
+
operation. That method's caller must also now take a callback, and so on up
|
20
|
+
the call chain. This means replacing a synchronous library such as
|
21
|
+
[Net::HTTP](http://ruby-doc.org/stdlib-1.8.7/libdoc/net/http/rdoc/index.html)
|
22
|
+
with an asynchronous library such as
|
23
|
+
[em-http-request](https://github.com/igrigorik/em-http-request) can require
|
24
|
+
rewriting a surprisingly large part of a codebase.
|
15
25
|
|
16
|
-
|
26
|
+
Ruby's block syntax initially seems to support this callback-passing style
|
27
|
+
well.
|
17
28
|
|
18
|
-
|
29
|
+
asynchronously_fetch_page do |page|
|
30
|
+
# do something with 'page'...
|
31
|
+
end
|
19
32
|
|
20
|
-
|
21
|
-
|
33
|
+
The first problem is that, unlike regular method parameters, Ruby doesn't check
|
34
|
+
that the caller remembered to provide a block, making it easy to create bugs by
|
35
|
+
forgetting a callback.
|
22
36
|
|
23
|
-
|
24
|
-
|
37
|
+
page = asynchronously_fetch_page
|
38
|
+
# returns immediately with no error. 'page' is probably nil.
|
25
39
|
|
26
|
-
|
27
|
-
|
40
|
+
Similarly the method implementer may forget to pass the callback down to nested
|
41
|
+
calls, which can render the whole chain of asynchronous methods unable to
|
42
|
+
return a result. (The asynchronous operation might itself check that a
|
43
|
+
callback was given, but asynchronous libraries will often not require a
|
44
|
+
callback, in case they were invoked only for their side-effects.)
|
28
45
|
|
46
|
+
def first_thing(&callback)
|
47
|
+
second_thing(&callback)
|
48
|
+
end
|
49
|
+
def second_thing(&callback)
|
50
|
+
third_thing # oops! no error though.
|
51
|
+
end
|
52
|
+
def third_thing(&callback)
|
53
|
+
compute_answer
|
54
|
+
yield 42 if block_given?
|
55
|
+
end
|
29
56
|
|
30
|
-
|
57
|
+
first_thing {|answer| puts answer } # never runs
|
31
58
|
|
32
|
-
|
33
|
-
|
59
|
+
This is a symptom of a more general problem: only the outermost caller really
|
60
|
+
cares about the callback being run, yet every method in the chain must be aware
|
61
|
+
of it, which is poor encapsulation.
|
34
62
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
shave
|
63
|
+
This style also breaks down when the asynchronous operation needs to
|
64
|
+
communicate failure: we want to pass in some code to be called on error, but
|
65
|
+
Ruby's syntax only allows passing a single block to a method, so callers now
|
66
|
+
need to pass in `lambda`s or hashes of `Proc`s, the syntax becomes inconsistent
|
67
|
+
and noisy, and readability and maintainability suffer:
|
41
68
|
|
42
|
-
|
69
|
+
def first_thing(errback, &callback)
|
70
|
+
do_something
|
71
|
+
yield if block_given? # as declared, callback is implicitly optional
|
72
|
+
rescue => e
|
73
|
+
errback.call(e) # as declared, errback is mandatory
|
74
|
+
end
|
43
75
|
|
44
|
-
|
45
|
-
|
76
|
+
# Excessive punctuation alert!
|
77
|
+
first_thing(lambda {|error| handle_error }) {|result| use_result(result) }
|
46
78
|
|
47
|
-
|
79
|
+
EventMachine offers the
|
80
|
+
[Deferrable](http://eventmachine.rubyforge.org/EventMachine/Deferrable.html)
|
81
|
+
pattern to communicate results of asynchronous operations in an object-oriented
|
82
|
+
style more natural to Ruby. Rather than taking a callback which it must
|
83
|
+
remember to call, the method simply returns a `Deferrable` object which
|
84
|
+
encapsulates the status of the operation, and promises to update that object at
|
85
|
+
a later date. Callers can register callbacks and errbacks on the Deferrable,
|
86
|
+
which takes care of calling them when the operation succeeds or fails.
|
87
|
+
Intermediate methods in the chain can simply pass the Deferrable on, and only
|
88
|
+
code which cares about callbacks need know about them.
|
48
89
|
|
49
|
-
|
50
|
-
|
51
|
-
|
90
|
+
However, asynchronous programming with Deferrables still suffers from two key
|
91
|
+
problems: it is difficult to compose multiple operations, and to build up
|
92
|
+
complex operations from simpler ones. Below is a method which performs three
|
93
|
+
synchronous operations in sequence, each depending on the result of the
|
94
|
+
previous, and returns the result of the last operation to the caller:
|
52
95
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
96
|
+
def complex_operation
|
97
|
+
first_result = do_first_thing
|
98
|
+
second_result = do_second_thing(first_result)
|
99
|
+
third_result = do_third_thing(second_result)
|
100
|
+
third_result
|
101
|
+
rescue => e
|
102
|
+
# ...
|
103
|
+
end
|
57
104
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
105
|
+
When the operations are asynchronous, the same sequence is typically
|
106
|
+
implemented using nested callbacks:
|
107
|
+
|
108
|
+
def complex_operation
|
109
|
+
result = EM::DefaultDeferrable.new
|
110
|
+
first_deferrable = do_first_thing
|
111
|
+
first_deferrable.callback do |first_result|
|
112
|
+
second_deferrable = do_second_thing(first_result)
|
113
|
+
second_deferrable.callback do |second_result|
|
114
|
+
third_deferrable = do_third_thing(second_result)
|
115
|
+
third_deferrable.callback do |third_result|
|
116
|
+
result.succeed(third_result)
|
117
|
+
end
|
118
|
+
third_deferrable.errback do |third_error|
|
119
|
+
result.fail(third_error)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
second_deferrable.errback do |second_error|
|
123
|
+
result.fail(second_error)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
first_deferrable.errback do |first_error|
|
127
|
+
result.fail(first_error)
|
63
128
|
end
|
129
|
+
result
|
130
|
+
end
|
131
|
+
|
132
|
+
Like the synchronous version, this method abstracts the multiple operations
|
133
|
+
away from the caller and presents only the result the caller was interested in
|
134
|
+
(or details of what went wrong). However, the line count has tripled. Worse,
|
135
|
+
the program flow is confusing: the logic of 'do these operations in sequence'
|
136
|
+
is obscured; the errbacks read in reverse order; and the way the final result
|
137
|
+
makes its way back to the caller is almost invisible. There are also a lot of
|
138
|
+
opportunities to create bugs: all of the callbacks must be manually and
|
139
|
+
repetitively "wired together", or the method will not work.
|
140
|
+
|
141
|
+
Deferrable Gratification aims to solve these problems by providing a library of
|
142
|
+
composition operators - combinators - which abstract away the boilerplate
|
143
|
+
callback wiring and reveal the logic of the code.
|
144
|
+
|
145
|
+
## Examples ##
|
146
|
+
|
147
|
+
### [Fluent callback syntax](http://samstokes.github.com/deferrable_gratification/doc/DeferrableGratification/Fluent.html): because it feels right ###
|
148
|
+
|
149
|
+
The return value of
|
150
|
+
[`Deferrable#callback`](http://eventmachine.rubyforge.org/EventMachine/Deferrable.html#M000264)
|
151
|
+
isn't useful, which means if you want to set a callback on a Deferrable and
|
152
|
+
then return it, you have to name the Deferrable first, so you can return it
|
153
|
+
explicitly. You also have to mention the name again to set another callback,
|
154
|
+
which leads to a lot of noisy repetition:
|
155
|
+
|
156
|
+
def google_homepage
|
157
|
+
request = EM::HttpRequest.new('http://google.com').get(:redirects => 1)
|
158
|
+
request.callback {|http| puts http.response }
|
159
|
+
request.callback { $call_count = ($call_count || 0) + 1 }
|
160
|
+
request.errback { puts "Oh noes!" }
|
161
|
+
request
|
162
|
+
end
|
163
|
+
EM.run { r = google_homepage; r.callback { EM.stop }; r.errback { EM.stop } }
|
164
|
+
# prints a lot of HTML
|
165
|
+
|
166
|
+
DG lets you chain callbacks and errbacks using "fluent syntax" familiar from
|
167
|
+
JQuery:
|
168
|
+
|
169
|
+
DG.enhance! EM::HttpClient
|
170
|
+
|
171
|
+
def google_homepage
|
172
|
+
EM::HttpRequest.new('http://google.com').get(:redirects => 1).
|
173
|
+
callback {|http| puts http.response }.
|
174
|
+
callback { $call_count = ($call_count || 0) + 1 }.
|
175
|
+
errback { puts "Oh noes!" }
|
176
|
+
end
|
177
|
+
EM.run { google_homepage.callback { EM.stop }.errback { EM.stop } }
|
178
|
+
# prints a lot of HTML
|
179
|
+
|
180
|
+
### [`bothback`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Bothback.html#bothback-instance\_method): when you absolutely, positively got to... ###
|
181
|
+
|
182
|
+
Sometimes you need to do something after an asynchronous action completes,
|
183
|
+
whether it succeeded or failed: e.g. release a lock, or as in the example
|
184
|
+
above, call `EM.stop` to break out of the `EM.run` block. It's annoying to
|
185
|
+
have to write that code twice, to make sure it's called both on success and
|
186
|
+
failure.
|
187
|
+
|
188
|
+
`bothback` to the rescue:
|
189
|
+
|
190
|
+
EM.run { google_homepage.bothback { EM.stop } }
|
191
|
+
# prints a lot of HTML
|
192
|
+
|
193
|
+
### [`transform`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Combinators.html#transform-instance\_method): receive the callbacks that make sense for you ###
|
194
|
+
|
195
|
+
The [em-http-request](https://github.com/igrigorik/em-http-request) library is
|
196
|
+
great, but it's a bit fiddly to use, because it passes the whole
|
197
|
+
[`EM::HttpClient`](http://rdoc.info/github/igrigorik/em-http-request/master/EventMachine/HttpClient)
|
198
|
+
instance to its callbacks and errbacks. That means your callbacks can check
|
199
|
+
the response code and headers, but it also makes it harder if you just want
|
200
|
+
quick access to the response body.
|
201
|
+
|
202
|
+
EM.run do
|
203
|
+
request = EM::HttpRequest.new('http://google.com').get(:redirects => 1)
|
204
|
+
request.
|
205
|
+
callback do |http|
|
206
|
+
# Have to write this code once for each different request
|
207
|
+
if http.response_header.status == 200
|
208
|
+
puts http.response
|
209
|
+
else
|
210
|
+
request.fail(http) # triggers the errback as if the request had failed
|
211
|
+
end
|
212
|
+
end.errback {|http| handle_error(http) }.
|
213
|
+
bothback { EM.stop }
|
214
|
+
end
|
215
|
+
# prints lots of HTML
|
216
|
+
|
217
|
+
Wouldn't it be great if we could encapsulate the logic of "just give me the
|
218
|
+
response if the request was successful", and have callbacks that just act on
|
219
|
+
the response body?
|
220
|
+
|
221
|
+
def fetch_page(url)
|
222
|
+
request = EM::HttpRequest.new(url).get(:redirects => 1)
|
223
|
+
request.transform do |http|
|
224
|
+
if http.response_header.status == 200
|
225
|
+
http.response
|
226
|
+
else
|
227
|
+
request.fail(http)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
EM.run do
|
233
|
+
fetch_page('http://google.com').
|
234
|
+
callback {|html| puts html }.
|
235
|
+
errback {|http| puts "Oh dear!" }.
|
236
|
+
bothback { EM.stop }
|
64
237
|
end
|
238
|
+
# prints lots of HTML
|
239
|
+
|
240
|
+
That looks a lot cleaner. It would be even cooler if instead of passing the
|
241
|
+
raw HTML to callbacks, we could parse the HTML using
|
242
|
+
[Hpricot](http://hpricot.com) and pass the parsed document instead. No
|
243
|
+
problem:
|
244
|
+
|
245
|
+
require 'hpricot'
|
246
|
+
|
247
|
+
def fetchpricot(url)
|
248
|
+
fetch_page(url).transform {|html| Hpricot(html) }
|
249
|
+
end
|
250
|
+
|
251
|
+
EM.run do
|
252
|
+
fetchpricot('http://google.com').
|
253
|
+
callback {|doc| puts doc.at(:title) }.
|
254
|
+
errback {|http| puts "Oh dear!" }.
|
255
|
+
bothback { EM.stop }
|
256
|
+
end
|
257
|
+
# prints <title>Google</title>
|
258
|
+
|
259
|
+
### [`transform_error`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Combinators.html#transform_error-instance\_method): receive the errbacks that make sense to you ###
|
260
|
+
|
261
|
+
That's cool, but it's a bit annoying that those errbacks receive a `HttpClient`
|
262
|
+
object - we have to turn that into a useful error message every time. Let's
|
263
|
+
encapsulate that too:
|
264
|
+
|
265
|
+
def fetchpricot2(url)
|
266
|
+
fetchpricot(url).transform_error do |http|
|
267
|
+
if http.response_header.status > 0
|
268
|
+
"Unexpected response code: #{http.response_header.status}"
|
269
|
+
else
|
270
|
+
"Unknown error!"
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
EM.run do
|
276
|
+
fetchpricot2('http://google.com/page_that_probably_does_not_exist').
|
277
|
+
callback {|doc| puts doc.at(:title) }.
|
278
|
+
errback {|error| puts "Error: #{error}" }.
|
279
|
+
bothback { EM.stop }
|
280
|
+
end
|
281
|
+
# prints "Error: Unexpected response code: 404"
|
282
|
+
|
283
|
+
### [`bind!`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Combinators.html#bind!-instance\_method): for when one thing leads to another ###
|
284
|
+
|
285
|
+
Say we want to do a simple web crawling task: find the first search result for
|
286
|
+
'deferrable_gratification', follow that link (which should be its Github page),
|
287
|
+
and pull down the project website listed on that page. Normally this would
|
288
|
+
require some messy nesting of callbacks and errbacks:
|
289
|
+
|
290
|
+
EM.run do
|
291
|
+
fetchpricot2('http://google.com/search?q=deferrable_gratification').callback do |doc1|
|
292
|
+
fetchpricot2((doc1 / 'ol' / 'li' / 'a')[0][:href]).callback do |doc2|
|
293
|
+
fetchpricot2((doc2 / '#repository_homepage').at(:a)[:href]).callback do |doc3|
|
294
|
+
puts doc3.at(:title).inner_text
|
295
|
+
# I could also have mistyped 'doc3' as 'doc2' and got the wrong
|
296
|
+
# behaviour, but no exception to flag it
|
297
|
+
end.errback do |error|
|
298
|
+
puts "Error finding homepage link: #{error}"
|
299
|
+
end.bothback { EM.stop }
|
300
|
+
end.errback do |error|
|
301
|
+
puts "Error loading first search result: #{error}"
|
302
|
+
EM.stop
|
303
|
+
end
|
304
|
+
end.errback do |error|
|
305
|
+
puts "Error retrieving search results: #{error}"
|
306
|
+
EM.stop
|
307
|
+
end
|
308
|
+
end
|
309
|
+
# prints "Deferrable Gratification"
|
310
|
+
|
311
|
+
With `Deferrable#bind!` we can remove the nesting and write something that
|
312
|
+
looks more like the straight-line sequential flow:
|
313
|
+
|
314
|
+
EM.run do
|
315
|
+
fetchpricot2('http://google.com/search?q=deferrable_gratification').bind! do |doc|
|
316
|
+
fetchpricot2((doc / 'ol' / 'li' / 'a')[0][:href])
|
317
|
+
end.bind! do |doc|
|
318
|
+
fetchpricot2((doc / '#repository_homepage').at(:a)[:href])
|
319
|
+
end.callback do |doc|
|
320
|
+
puts doc.at(:title).inner_text
|
321
|
+
# now the previous 'doc's aren't in scope, so I can't accidentally
|
322
|
+
# refer to them
|
323
|
+
end.errback do |error|
|
324
|
+
puts "Error: #{error}"
|
325
|
+
end.bothback { EM.stop }
|
326
|
+
end
|
327
|
+
# prints "Deferrable Gratification"
|
328
|
+
|
329
|
+
`bind!` also wires up the errbacks so we can just write a single errback that
|
330
|
+
will fire if any step in the sequence fails; similarly we don't have to write
|
331
|
+
`EM.stop` three times.
|
332
|
+
|
333
|
+
## Getting started ##
|
334
|
+
|
335
|
+
Install the gem:
|
336
|
+
|
337
|
+
gem install deferrable_gratification
|
338
|
+
|
339
|
+
In your code:
|
340
|
+
|
341
|
+
require 'eventmachine'
|
342
|
+
require 'deferrable_gratification'
|
343
|
+
DG.enhance_all_deferrables!
|
344
|
+
|
345
|
+
Make sure that the call to
|
346
|
+
[`DG.enhance_all_deferrables!`](http://samstokes.github.com/deferrable_gratification/doc/DeferrableGratification.html#enhance_all_deferrables%21-class_method)
|
347
|
+
comes *before* you require any library that uses `Deferrable` (e.g.
|
348
|
+
[em-http-request](https://github.com/igrigorik/em-http-request)).
|
349
|
+
|
350
|
+
### Temporary workaround because `enhance_all_deferrables!` is broken ###
|
351
|
+
|
352
|
+
You actually need to call
|
353
|
+
[`DG.enhance!`](http://samstokes.github.com/deferrable_gratification/doc/DeferrableGratification.html#enhance%21-class_method)
|
354
|
+
on each Deferrable class you'll be dealing with. This call needs to come
|
355
|
+
*after* that class is defined.
|
356
|
+
|
357
|
+
## Documentation ##
|
358
|
+
|
359
|
+
* [API](http://samstokes.github.com/deferrable_gratification/doc/frames.html)
|
360
|
+
* [Behaviour specs](http://samstokes.github.com/deferrable_gratification/doc/spec/index.html)
|
361
|
+
(generated from RSpec code examples)
|
@@ -12,7 +12,7 @@ module DeferrableGratification
|
|
12
12
|
# def product_names_for_username(username)
|
13
13
|
# DB.query('SELECT id FROM users WHERE username = ?', username).bind! do |user_id|
|
14
14
|
# DB.query('SELECT name FROM products WHERE user_id = ?', user_id)
|
15
|
-
# end.
|
15
|
+
# end.transform do |product_names|
|
16
16
|
# product_names.join(', ')
|
17
17
|
# end
|
18
18
|
# end
|
@@ -131,17 +131,39 @@ module DeferrableGratification
|
|
131
131
|
# Deferrable will also fail.
|
132
132
|
#
|
133
133
|
# @param &block block that transforms the expected result of this
|
134
|
-
#
|
134
|
+
# operation in some way.
|
135
135
|
#
|
136
136
|
# @return [Deferrable] Deferrable that will succeed if this operation did,
|
137
137
|
# after transforming its result.
|
138
138
|
#
|
139
139
|
# @example Retrieve a web page and call back with its title.
|
140
|
-
# HTTP.request(url).
|
141
|
-
def
|
140
|
+
# HTTP.request(url).transform {|page| Hpricot(page).at(:title).inner_html }
|
141
|
+
def transform(&block)
|
142
142
|
bind!(&block)
|
143
143
|
end
|
144
144
|
|
145
|
+
# Transform the value passed to the errback of this Deferrable by invoking
|
146
|
+
# +block+. If this operation succeeds, the returned Deferrable will
|
147
|
+
# succeed with the same value. If this operation fails, the returned
|
148
|
+
# Deferrable will fail with the transformed error value.
|
149
|
+
#
|
150
|
+
# @param &block block that transforms the expected error value of this
|
151
|
+
# operation in some way.
|
152
|
+
#
|
153
|
+
# @return [Deferrable] Deferrable that will succeed if this operation did,
|
154
|
+
# otherwise fail after transforming the error value with which this
|
155
|
+
# operation failed.
|
156
|
+
def transform_error(&block)
|
157
|
+
errback do |*err|
|
158
|
+
self.fail(
|
159
|
+
begin
|
160
|
+
yield(*err)
|
161
|
+
rescue => e
|
162
|
+
e
|
163
|
+
end)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
145
167
|
|
146
168
|
# Boilerplate hook to extend {ClassMethods}.
|
147
169
|
def self.included(base)
|
@@ -167,6 +189,93 @@ module DeferrableGratification
|
|
167
189
|
def chain(*actions)
|
168
190
|
actions.inject(DG.const(nil), &:>>)
|
169
191
|
end
|
192
|
+
|
193
|
+
# Combinator that waits for all of the supplied asynchronous operations
|
194
|
+
# to succeed or fail, then succeeds with the results of all those
|
195
|
+
# operations that were successful.
|
196
|
+
#
|
197
|
+
# This Deferrable will never fail. It may also never succeed, if _any_
|
198
|
+
# of the supplied operations does not either succeed or fail.
|
199
|
+
#
|
200
|
+
# The successful results are guaranteed to be in the same order as the
|
201
|
+
# operations were passed in (which may _not_ be the same as the
|
202
|
+
# chronological order in which they succeeded).
|
203
|
+
#
|
204
|
+
# @param [*Deferrable] *operations deferred statuses of asynchronous
|
205
|
+
# operations to wait for.
|
206
|
+
#
|
207
|
+
# @return [Deferrable] a deferred status that will succeed after all the
|
208
|
+
# +operations+ have either succeeded or failed; its callbacks will be
|
209
|
+
# passed an +Enumerable+ containing the results of those operations
|
210
|
+
# that succeeded.
|
211
|
+
def join_successes(*operations)
|
212
|
+
Join::Successes.setup!(*operations)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Combinator that waits for any of the supplied asynchronous operations
|
216
|
+
# to succeed, and succeeds with the result of the first (chronologically)
|
217
|
+
# to do so.
|
218
|
+
#
|
219
|
+
# This Deferrable will fail if all the operations fail. It may never
|
220
|
+
# succeed or fail, if one of the operations also does not.
|
221
|
+
#
|
222
|
+
# @param (see #join_successes)
|
223
|
+
#
|
224
|
+
# @return [Deferrable] a deferred status that will succeed as soon as any
|
225
|
+
# of the +operations+ succeeds; its callbacks will be passed the result
|
226
|
+
# of that operation.
|
227
|
+
def join_first_success(*operations)
|
228
|
+
Join::FirstSuccess.setup!(*operations)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Combinator that repeatedly executes the supplied block until it
|
232
|
+
# succeeds, then succeeds itself with the eventual result.
|
233
|
+
#
|
234
|
+
# This Deferrable may never succeed, if the operation never succeeds.
|
235
|
+
# It will fail if an iteration raises an exception.
|
236
|
+
#
|
237
|
+
# @note this combinator is intended for use inside EventMachine. It will
|
238
|
+
# still work outside of EventMachine, _provided_ that the operation is
|
239
|
+
# synchronous (although a simple +while+ loop might be preferable in
|
240
|
+
# this case!).
|
241
|
+
#
|
242
|
+
# @param loop_deferrable for internal use only, always omit this.
|
243
|
+
# @param &block operation to execute until it succeeds.
|
244
|
+
#
|
245
|
+
# @yieldreturn [Deferrable] deferred status of the operation. If it
|
246
|
+
# fails, the operation will be retried. If it succeeds, the combinator
|
247
|
+
# will succeed with the result.
|
248
|
+
#
|
249
|
+
# @return [Deferrable] a deferred status that will succeed once the
|
250
|
+
# supplied operation eventually succeeds.
|
251
|
+
def loop_until_success(loop_deferrable = DefaultDeferrable.new, &block)
|
252
|
+
if EM.reactor_running?
|
253
|
+
EM.next_tick do
|
254
|
+
begin
|
255
|
+
attempt = yield
|
256
|
+
rescue => e
|
257
|
+
loop_deferrable.fail(e)
|
258
|
+
else
|
259
|
+
attempt.callback(&loop_deferrable.method(:succeed))
|
260
|
+
attempt.errback { loop_until_success(loop_deferrable, &block) }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
else
|
264
|
+
# In the synchronous case, we could simply use the same
|
265
|
+
# implementation as in EM, but without the next_tick; unfortunately
|
266
|
+
# that means direct recursion, so risks stack overflow. Instead we
|
267
|
+
# just reimplement as a loop.
|
268
|
+
results = []
|
269
|
+
begin
|
270
|
+
yield.callback {|*values| results << values } while results.empty?
|
271
|
+
rescue => e
|
272
|
+
loop_deferrable.fail(e)
|
273
|
+
else
|
274
|
+
loop_deferrable.succeed(*results[0])
|
275
|
+
end
|
276
|
+
end
|
277
|
+
loop_deferrable
|
278
|
+
end
|
170
279
|
end
|
171
280
|
end
|
172
281
|
end
|
@@ -31,7 +31,7 @@ module DeferrableGratification
|
|
31
31
|
# However, because Ruby doesn't actually type-check blocks, we can't
|
32
32
|
# enforce that the block really does return a second Deferrable. This
|
33
33
|
# therefore also supports (reasonably) arbitrary blocks. However, it's
|
34
|
-
# probably clearer (though equivalent) to use {#
|
34
|
+
# probably clearer (though equivalent) to use {#transform} for this case.
|
35
35
|
class Bind < DefaultDeferrable
|
36
36
|
# Prepare to bind +block+ to +first+, and create the Deferrable that
|
37
37
|
# will represent the bind.
|
@@ -84,7 +84,7 @@ module DeferrableGratification
|
|
84
84
|
second.errback {|*error| self.fail(*error) }
|
85
85
|
else
|
86
86
|
# Not a Deferrable, so we need to "behave sensibly" as alluded to
|
87
|
-
# above. Just behaving like #
|
87
|
+
# above. Just behaving like #transform is sensible enough.
|
88
88
|
self.succeed(second)
|
89
89
|
end
|
90
90
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. default_deferrable])
|
2
|
+
|
3
|
+
module DeferrableGratification
|
4
|
+
module Combinators
|
5
|
+
# Abstract base class for combinators that depend on a number of
|
6
|
+
# asynchronous operations (potentially executing in parallel).
|
7
|
+
#
|
8
|
+
# @abstract Subclasses should override {#done?} to define whether they wait
|
9
|
+
# for some or all of the operations to complete, and {#finish} to define
|
10
|
+
# what they do when {#done?} returns true.
|
11
|
+
class Join < DefaultDeferrable
|
12
|
+
# Prepare to wait for the completion of +operations+.
|
13
|
+
#
|
14
|
+
# Does not actually set up any callbacks or errbacks: call {#setup!} for
|
15
|
+
# that.
|
16
|
+
#
|
17
|
+
# @param [*Deferrable] *operations deferred statuses of asynchronous
|
18
|
+
# operations to wait for.
|
19
|
+
def initialize(*operations)
|
20
|
+
@operations = operations
|
21
|
+
@successes = Array.new(@operations.size, Sentinel.new)
|
22
|
+
@failures = Array.new(@operations.size, Sentinel.new)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Register callbacks and errbacks on the supplied operations to notify
|
26
|
+
# this {Join} of completion.
|
27
|
+
def setup!
|
28
|
+
finish if done?
|
29
|
+
|
30
|
+
@operations.each_with_index do |op, index|
|
31
|
+
op.callback do |result|
|
32
|
+
@successes[index] = result
|
33
|
+
finish if done?
|
34
|
+
end
|
35
|
+
op.errback do |error|
|
36
|
+
@failures[index] = error
|
37
|
+
finish if done?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create a {Join} and register the callbacks.
|
43
|
+
#
|
44
|
+
# @param (see #initialize)
|
45
|
+
#
|
46
|
+
# @return [Join] Deferrable representing the join operation.
|
47
|
+
def self.setup!(*operations)
|
48
|
+
new(*operations).tap(&:setup!)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Combinator that waits for all of the supplied asynchronous operations
|
53
|
+
# to succeed or fail, then succeeds with the results of all those
|
54
|
+
# operations that were successful.
|
55
|
+
#
|
56
|
+
# This Deferrable will never fail. It may also never succeed, if _any_
|
57
|
+
# of the supplied operations does not either succeed or fail.
|
58
|
+
#
|
59
|
+
# The successful results are guaranteed to be in the same order as the
|
60
|
+
# operations were passed in (which may _not_ be the same as the
|
61
|
+
# chronological order in which they succeeded).
|
62
|
+
#
|
63
|
+
# You probably want to call {ClassMethods#join_successes} rather than
|
64
|
+
# using this class directly.
|
65
|
+
class Successes < Join
|
66
|
+
private
|
67
|
+
def done?
|
68
|
+
all_completed?
|
69
|
+
end
|
70
|
+
|
71
|
+
def finish
|
72
|
+
succeed(successes)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Combinator that waits for any of the supplied asynchronous operations
|
78
|
+
# to succeed, and succeeds with the result of the first (chronologically)
|
79
|
+
# to do so.
|
80
|
+
#
|
81
|
+
# This Deferrable will fail if all the operations fail. It may never
|
82
|
+
# succeed or fail, if one of the operations also does not.
|
83
|
+
#
|
84
|
+
# You probably want to call {ClassMethods#join_first_success} rather than
|
85
|
+
# using this class directly.
|
86
|
+
class FirstSuccess < Join
|
87
|
+
private
|
88
|
+
def done?
|
89
|
+
successes.length > 0
|
90
|
+
end
|
91
|
+
|
92
|
+
def finish
|
93
|
+
succeed(successes.first)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
private
|
99
|
+
def successes
|
100
|
+
without_sentinels(@successes)
|
101
|
+
end
|
102
|
+
|
103
|
+
def failures
|
104
|
+
without_sentinels(@failures)
|
105
|
+
end
|
106
|
+
|
107
|
+
def all_completed?
|
108
|
+
successes.length + failures.length >= @operations.length
|
109
|
+
end
|
110
|
+
|
111
|
+
def done?
|
112
|
+
raise NotImplementedError, 'subclasses should override this'
|
113
|
+
end
|
114
|
+
|
115
|
+
def finish
|
116
|
+
raise NotImplementedError, 'subclasses should override this'
|
117
|
+
end
|
118
|
+
|
119
|
+
def without_sentinels(ary)
|
120
|
+
ary.reject {|item| item.instance_of? Sentinel }
|
121
|
+
end
|
122
|
+
|
123
|
+
# @private
|
124
|
+
# Used internally to distinguish between the absence of a response and
|
125
|
+
# a response with the value +nil+.
|
126
|
+
class Sentinel; end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deferrable_gratification
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Sam Stokes
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-01-
|
18
|
+
date: 2011-01-21 00:00:00 -08:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -91,9 +91,9 @@ dependencies:
|
|
91
91
|
type: :development
|
92
92
|
version_requirements: *id005
|
93
93
|
description: |
|
94
|
-
Deferrable Gratification (DG)
|
94
|
+
Deferrable Gratification (DG) facilitates asynchronous programming in Ruby, by helping create abstractions around complex operations built up from simpler ones. It helps make asynchronous code less error-prone and easier to compose. It also provides some enhancements to the Deferrable API.
|
95
95
|
|
96
|
-
|
96
|
+
Features include:
|
97
97
|
|
98
98
|
* fluent (aka chainable) syntax for registering multiple callbacks and errbacks to the same Deferrable.
|
99
99
|
|
@@ -113,6 +113,7 @@ files:
|
|
113
113
|
- lib/deferrable_gratification.rb
|
114
114
|
- lib/deferrable_gratification/default_deferrable.rb
|
115
115
|
- lib/deferrable_gratification/combinators/bind.rb
|
116
|
+
- lib/deferrable_gratification/combinators/join.rb
|
116
117
|
- lib/deferrable_gratification/combinators.rb
|
117
118
|
- lib/deferrable_gratification/bothback.rb
|
118
119
|
- lib/deferrable_gratification/fluent.rb
|