deferred 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/MIT_LICENSE +19 -0
- data/README.markdown +215 -0
- data/Rakefile +1 -0
- data/deferred.gemspec +23 -0
- data/examples/threadpool_job_subtask.rb +203 -0
- data/lib/deferred.rb +32 -0
- data/lib/deferred/accessors.rb +170 -0
- data/lib/deferred/default.rb +36 -0
- data/lib/deferred/extensions.rb +22 -0
- data/lib/deferred/instance_methods.rb +82 -0
- data/lib/deferred/threadpool_job.rb +63 -0
- data/lib/deferred/version.rb +3 -0
- data/spec/deferred/accessors_spec.rb +95 -0
- data/spec/deferred/instance_methods_spec.rb +297 -0
- data/spec/deferred/threadpool_job_spec.rb +172 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/accessorized.rb +8 -0
- data/spec/support/bollocks_error.rb +2 -0
- data/spec/support/deferred_state_xtn.rb +22 -0
- metadata +166 -0
data/.rspec
ADDED
data/Gemfile
ADDED
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
|
+
|