deferrable_gratification 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
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
|