deferred 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in deferred.gemspec
4
+ gemspec
data/MIT_LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by Hewlett Packard Development Company, L.P.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,215 @@
1
+ ## Deferred: A bit of sugar to make async more pleasant
2
+
3
+ Hey, EventMachine is pretty cool, scales really well, etc. The only thing is that you have to write all your code using deferreds, which can be kind of...challenging. This library will not solve all your problems, it will not walk your dog, it does not do windows. However, it provides syntactic sugar around some core EventMachine libraries to make things a bit more pleasant.
4
+
5
+ ### Deferred::InstanceMethods
6
+
7
+ This module includes EM::Deferrable, and tweaks the behavior a little. First off the `callback` and `errback` methods will return `self` which allows for chaining. In addition, an `ensure`-like method has been added `ensure_that` that adds a block to both the `callback` and `errback` chains.
8
+
9
+ ```ruby
10
+ client.do_something_eventually.callback do |arg|
11
+ something_happened(arg)
12
+ end.errback do |err|
13
+ raise err
14
+ end.ensure_that |*|
15
+ cleanup!
16
+ end
17
+ ```
18
+
19
+ Sometimes you have a deferred that needs to wait on another deferred to fire before it takes action, for this a `chain\_to` method is provided.
20
+
21
+ ```ruby
22
+ def something
23
+ Deferred::Default.new.tap do |my_dfr|
24
+ my_dfr.chain_to(client.do_something_eventually)
25
+ end
26
+ end
27
+ ```
28
+
29
+ EM::Deferred objects have a timeout method, but they don't let you specify an error class to use. In our code, all of our errbacks take an argument that is an Exception instance. Deferred::InstanceMethods#timeout allows you to optionally specify an Exception subclass that will be passed to registered errbacks after the given timeout is fired.
30
+
31
+ ```ruby
32
+ dfr = client.do_something_eventually
33
+ dfr.callback { |arg| something_happened!(arg) }
34
+ dfr.errback { |e| puts e.inspect }
35
+ dfr.timeout(3.0, MyTimeoutError)
36
+ ```
37
+
38
+ In this case, if a timeout occurs, the errback would be called with a MyTimeoutError instance with the message "timeout after 3.0 seconds".
39
+
40
+ Finally, there's the `errback\_on\_exception` method, that lets you fire the errback chain if an exception is raised in the given code block.
41
+
42
+ ```ruby
43
+ dfr.errback_on_exception do
44
+ # try a bunch of stuff
45
+ raise "Oh noes! something bad!"
46
+ end
47
+ ```
48
+
49
+ In this case, registered errbacks would be called with a RuntimeError.
50
+
51
+
52
+ ### Deferred::Accessors
53
+
54
+ A common pattern is providing lifecycle events on a given instance, clients connect, disconnect, startup, shutdown, error, etc. The `Deferred::Accessor.deferred\_event` class method provides a convenient way of declaring these deferreds in a class.
55
+
56
+ ```ruby
57
+ class SomethingManager
58
+ include Deferred::Accessors
59
+
60
+ deferred_event :start
61
+
62
+ def start
63
+ # do stuff required for starting the SomethingManager
64
+
65
+ on_start.succeed
66
+ end
67
+ end
68
+ ```
69
+
70
+ It's often more convenient to combine the callback with the call to initiate the action, for example:
71
+
72
+ ```ruby
73
+ class SomethingManager
74
+ include Deferred::Accessors
75
+
76
+ deferred_event :start
77
+
78
+ def initialize
79
+ @started = false
80
+ end
81
+
82
+ def start(&cb)
83
+ on_start(&cb) # registers cb as a callback
84
+
85
+ return on_start if @started
86
+ @started = true
87
+
88
+ # this obviously would be some kind of deferred action that would
89
+ # eventually call on_start.succeed
90
+ #
91
+ EM.next_tick { on_start.succeed }
92
+
93
+ on_start
94
+ end
95
+ end
96
+ ```
97
+
98
+ This allows one to do the following:
99
+
100
+ ```ruby
101
+ s = SomethingManager.new
102
+ s.start do
103
+ do_something_when_manager_started
104
+ end.errback do
105
+ raise "Oh Noes! Manager failed to start! Abort!"
106
+ end
107
+ ```
108
+
109
+ ## Deferred::ThreadpoolJob
110
+
111
+ A common need is to run some blocking task in the EventMachine threadpool using `EM.defer`. This works well, but there's no way of handling error cases, as when an exception is raised in the threadpool, the reactor dies. Enter the `ThreadpoolJob`, a Deferred that wraps the running of a job in the threadpool.
112
+
113
+ ### The success case
114
+
115
+ ```ruby
116
+ require 'rubygems'
117
+ require 'eventmachine'
118
+ require 'deferred'
119
+
120
+ EM.run do
121
+ Deferred::DefaultThreadpoolJob.new.tap do |tpj|
122
+ tpj.before_run do
123
+ $stderr.puts "w00t! we're about to run in the threadpool, EM.reactor_thread? #{EM.reactor_thread?}"
124
+ end
125
+
126
+ tpj.on_run do
127
+ $stderr.puts "EM.reactor_thread? #{EM.reactor_thread?}"
128
+ $stderr.puts "sleeping for 0.5s"
129
+ sleep 0.5
130
+
131
+ %w[this is the result of the run]
132
+ end
133
+
134
+ tpj.callback do |*a|
135
+ $stderr.puts "Success, called with: #{a.inspect}"
136
+ end
137
+
138
+ tpj.errback do |exc|
139
+ $stderr.puts "Oh noes! an error happened: #{exc.inspect}"
140
+ end
141
+
142
+ # we have all the Deferred::InstanceMethods
143
+ tpj.ensure_that do |*|
144
+ EM.next_tick { EM.stop_event_loop }
145
+ end
146
+
147
+ tpj.defer!
148
+ end
149
+ end
150
+ ```
151
+
152
+ Produces the output:
153
+
154
+ ```
155
+ w00t! we're about to run in the threadpool, EM.reactor_thread? true
156
+ EM.reactor_thread? false
157
+ sleeping for 0.5s
158
+ Success, called with: [["this", "is", "the", "result", "of", "the", "run"]]
159
+ ```
160
+
161
+ ### The error case
162
+
163
+ ```ruby
164
+ require 'rubygems'
165
+ require 'eventmachine'
166
+ require 'deferred'
167
+
168
+ EM.run do
169
+ Deferred::DefaultThreadpoolJob.new.tap do |tpj|
170
+ tpj.before_run do
171
+ $stderr.puts "w00t! we're about to run in the threadpool, EM.reactor_thread? #{EM.reactor_thread?}"
172
+ end
173
+
174
+ tpj.on_run do
175
+ raise "Teh goggles, they do nothing!"
176
+ end
177
+
178
+ tpj.callback do |*a|
179
+ $stderr.puts "Success, called with: #{a.inspect}"
180
+ end
181
+
182
+ tpj.errback do |exc|
183
+ $stderr.puts "Oh noes! an error happened: #{exc.inspect}"
184
+ end
185
+
186
+ # we have all the Deferred::InstanceMethods
187
+ tpj.ensure_that do |*|
188
+ EM.next_tick { EM.stop_event_loop }
189
+ end
190
+
191
+ tpj.defer!
192
+ end
193
+ end
194
+ ```
195
+
196
+ Produces the output:
197
+
198
+ ```
199
+ w00t! we're about to run in the threadpool, EM.reactor_thread? true
200
+ Oh noes! an error happened: #<RuntimeError: Teh goggles, they do nothing!>
201
+ ```
202
+
203
+ ### Returning a deferred or an object that responds\_to?(:defer!)
204
+
205
+ One use case we had was the ability for a ThreadpoolJob's on\_run block to return a deferred, which would be chained to the ThreadpoolJob's callbacks. This allows a lot of flexibility in terms of chaining tasks that should run both in and out of the threadpool.
206
+
207
+ This functionality was created for a task queueing system where *most* of the jobs had to run on the threadpool. Check out the rather [involved example][] which shows how to implement a DefaultThreadpoolJob that spawns several sub-jobs and waits for their completion before firing its own callbacks.
208
+
209
+
210
+ ## From the "Credit Where Credit is Due Dept." ##
211
+
212
+ Thanks to [Snapfish][] for sponsoring development of this project and to HPDC, L.P. for agreeing to open source it.
213
+
214
+ [Snapfish]: http://www.snapfish.com
215
+ [involved example]: https://github.com/motionbox/deferred/blob/master/examples/threadpool_job_subtask.rb
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/deferred.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "deferred/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "deferred"
7
+ s.version = Deferred::VERSION
8
+ s.authors = ["Jonathan D. Simms"]
9
+ s.email = ["simms@hp.com"]
10
+ s.summary = %q{Syntactical sugar around an EM::Deferrable}
11
+ s.description = s.summary + "\n"
12
+
13
+ s.add_dependency('eventmachine', '>= 0.12.10')
14
+
15
+ s.add_development_dependency('rspec', '>= 2.5.0', '< 3.0.0')
16
+ s.add_development_dependency('ZenTest', '>= 4.5.0')
17
+ s.add_development_dependency('evented-spec', '~> 0.4.1')
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ end
@@ -0,0 +1,203 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'deferred'
4
+
5
+ require 'logger'
6
+
7
+ $log = Logger.new($stderr).tap { |l| l.level = Logger::DEBUG }
8
+
9
+ # This is a real-world example of how we use this gem in a production setting.
10
+ #
11
+ # Surrounding this would be a daemon that's listening for events that represent
12
+ # asynchronous tasks that need to be run (what the simplified TaskRunner class
13
+ # below represents). We have components in our stack that are not evented, so we
14
+ # need to run tasks on the threadpool. In addition, some of those tasks (like the
15
+ # MainTask below) represent a "meta" task, a task that may have several
16
+ # sub-components that can be run simultaneously. An example of this kind of task
17
+ # would be one that needs to retrieve multiple resources from the web. The "meta"
18
+ # task is "retrieve all sources," which can be divided into N number of tasks that
19
+ # can be run concurrently.
20
+ #
21
+ # This code performs the following steps:
22
+ #
23
+ # * The TaskRunner creates a MainTask, and sets things up so that it can run its
24
+ # process! method in the threadpool
25
+ #
26
+ # * The TaskRunner block is scheduled and the MainTask is run
27
+ #
28
+ # * The MainTask process! method:
29
+ # * sets up a callback that will call the handle_success method on successful
30
+ # completion of all subtasks (using the on_iterator_finished event)
31
+ # * sets up a callback that will call handle_failure in the case of any
32
+ # SubThreadTask failure (using the on_iterator_finished event)
33
+ # * sets up a next_tick block and returns self, effectively freeing the
34
+ # thread. (This is technically kind of a waste of the use of a thread, but
35
+ # for consistency with the rest of our task system, it's easier than
36
+ # special casing this)
37
+ #
38
+ # * Since MainTask is a Deferred, the TaskRunner will chain itself to the MainTask,
39
+ # meaning that when the MainTask calls back, the TaskRunner will inspect its return
40
+ # value and take the appropriate action (it will either be done, or if it's another
41
+ # Deferred it will wait on *that*, and if it's also a ThreadpoolJob it will
42
+ # call defer! on that return value).
43
+ #
44
+ # * the reactor ticks
45
+ #
46
+ # * The next_tick block runs an EM::Iterator to create SubThreadTasks which
47
+ # will be run in their own threads. When all subtasks are complete it fires
48
+ # the on_iterator_finished event. If any SubThreadTask fails,
49
+ # on_iterator_finished will errback immediately, all running tasks will
50
+ # complete, but will be ignored.
51
+ #
52
+ # * The SubThreadTasks all complete
53
+ #
54
+ # * on_iterator_finished is fired and in order to handle the result MainTask will
55
+ # need to perform another ThreadpoolJob. We set up the ThreadpoolJob, and call
56
+ # succeed on the MainTask with that ThreadpoolJob as an argument. The MainTask
57
+ # will now depend on that job's succesful completion
58
+ #
59
+ # * the cleanup task runs in a separate thread
60
+ #
61
+ # * the cleanup task completes and the main TaskRunner job is done.
62
+ #
63
+ #
64
+ # Now, this may seem like complete overkill, however it shows how flexible the
65
+ # ThreadpoolJob is (credit to Topper Bowers who figured out the recursive
66
+ # behavior for ThreadpoolJob#handle_result, ALL PRAISE TO THE HYPNOTOAD!). We
67
+ # have a myriad of different solutions, some sync, some async. The flexibility
68
+ # of Deferred allows us to leverage EventMachine to manage the overall
69
+ # concurrency in a sensible way, but use the threadpool freely, and therefore
70
+ # give us an incredibly powerful tool.
71
+ #
72
+ module ThreadpoolJobSubtasks
73
+ class SubThreadTask
74
+ include Deferred::ThreadpoolJob
75
+
76
+ attr_accessor :pause_for_sec
77
+
78
+ def initialize(pause_for_sec)
79
+ @pause_for_sec = pause_for_sec
80
+ @on_run_block = method(:process!).to_proc
81
+ end
82
+
83
+ def process!
84
+ $log.debug { "SubThreadTask: Thread id: %16x sleeping %0.9f sec" % [Thread.current.object_id, @pause_for_sec] }
85
+ sleep(@pause_for_sec)
86
+ self.succeed("SubThreadTask: Thread id: %16x complete %0.9f sec" % [Thread.current.object_id, @pause_for_sec] )
87
+ end
88
+ end
89
+
90
+ class MainTask
91
+ include Deferred
92
+ include Deferred::Accessors
93
+
94
+ deferred_event :iterator_finished
95
+
96
+ def initialize
97
+ @durations = []
98
+ end
99
+
100
+ def wait_for_durations=(durations)
101
+ @durations = durations
102
+ end
103
+
104
+ def process!
105
+ $log.debug { "MainTask#process! called, going to schedule sleeps for #{@durations.inspect} seconds concurrently" }
106
+
107
+ on_iterator_finished.callback { |*| handle_success }
108
+ on_iterator_finished.errback { |e| handle_failure(e) }
109
+
110
+ EM.next_tick do
111
+ EM::Iterator.new(@durations.dup, 5).each(
112
+ lambda { |dur,iter|
113
+ tpj = SubThreadTask.new(dur)
114
+ tpj.before_run.callback { $log.debug { "SubThreadTask: about to sleep #{dur}" } }
115
+ tpj.callback { |s| $log.debug(s); @durations.delete(dur); iter.next }
116
+ tpj.errback { |e| on_iterator_finished.fail(e) }
117
+ tpj.defer!
118
+ },
119
+ lambda {
120
+ $log.debug { "MainTask: all SubThreadTasks completed, @durations: #{@durations.inspect}" }
121
+ on_iterator_finished.succeed
122
+ }
123
+ )
124
+
125
+ end
126
+
127
+ self
128
+ end
129
+
130
+ # on success, we need to perform another blocking action, so we return a
131
+ # ThreadpoolJob instance (actually, anything that responds_to?(:defer!))
132
+ def handle_success
133
+ $log.debug { "MainTask: all SubThreadTasks have completed running successfully, now we do some other action on the threadpool before we're done" }
134
+
135
+ Deferred::DefaultThreadpoolJob.new.tap do |dtj|
136
+ dtj.on_run do
137
+ sleep(1.0)
138
+ end
139
+
140
+ dtj.before_run do
141
+ $log.debug { "MainTask: start some blocking action after success" }
142
+ end
143
+
144
+ dtj.callback do
145
+ $log.debug { "MainTask: some blocking action after success finished" }
146
+ end
147
+
148
+ dtj.errback { |e| raise e }
149
+
150
+ # here we do something fairly tricky.
151
+ # since this is all done for side-effect, we can signal that we're done processing
152
+ # (i.e. the main task is complete), but do some kind of asynchronous "next task"
153
+ # by returning a ThreadpoolJob.
154
+ $log.debug { "MainTask: succeeding the main task with another ThreadpoolJob" }
155
+
156
+ self.succeed(dtj)
157
+ end
158
+ end
159
+
160
+ def handle_failure(exc)
161
+ $log.debug { "Oh noes! an error occurred! #{exc.inspect}" }
162
+ self.fail(exc)
163
+ end
164
+ end
165
+
166
+ # This needs to run MainTask so that we don't get re-scheduled over and over
167
+ # (as if MainTask responds_to?(:defer!) it'll get deferred repeatedly)
168
+ class TaskRunner
169
+ include Deferred::ThreadpoolJob
170
+ end
171
+
172
+ def self.run!
173
+ sub_task_durations = []
174
+
175
+ 20.times { sub_task_durations << rand }
176
+
177
+ EM.run do
178
+ main_task = MainTask.new.tap do |main|
179
+ main.wait_for_durations = sub_task_durations
180
+ end
181
+
182
+ TaskRunner.new.tap do |tr|
183
+ tr.on_run { main_task.process! }
184
+
185
+ tr.callback { $log.debug { "TaskRunner: main task completed" } }
186
+ tr.errback { |e| raise e }
187
+
188
+ tr.ensure_that do
189
+ EM.next_tick do
190
+ $log.debug { "TaskRunner: we're outta here" }
191
+ EM.stop_event_loop
192
+ end
193
+ end
194
+
195
+ tr.defer!
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ ThreadpoolJobSubtasks.run!
202
+
203
+