task_batcher 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +30 -0
- data/README.md +118 -0
- data/lib/task_batcher.rb +301 -0
- data/task_batcher.gemspec +41 -0
- metadata +116 -0
data/LICENSE
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
Released under the three-clause BSD open source license.
|
2
|
+
http://opensource.org/licenses/BSD-3-Clause
|
3
|
+
|
4
|
+
Copyright (c) 2013, Apartment List
|
5
|
+
All rights reserved.
|
6
|
+
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
9
|
+
|
10
|
+
* Redistributions of source code must retain the above copyright notice, this
|
11
|
+
list of conditions and the following disclaimer.
|
12
|
+
|
13
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
15
|
+
and/or other materials provided with the distribution.
|
16
|
+
|
17
|
+
* Neither the name of Apartment List nor the names of its contributors may
|
18
|
+
be used to endorse or promote products derived from this software without
|
19
|
+
specific prior written permission.
|
20
|
+
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
TaskBatcher
|
2
|
+
===
|
3
|
+
|
4
|
+
Some tasks, like database inserts, are much more efficient to process in a
|
5
|
+
batch. However, we generally want our tasks to be processed "soon" even if
|
6
|
+
there's only one task. The TaskBatcher gem groups tasks by a taskname
|
7
|
+
parameter, and starts a timer when the first task comes in. After the batch
|
8
|
+
timer expires, it processes all tasks that it received in that time. (The
|
9
|
+
caller provides the block to process the tasks.)
|
10
|
+
|
11
|
+
Uses EventMachine under the hood. May be combined with Messenger for
|
12
|
+
durability guarantees.
|
13
|
+
|
14
|
+
Tested under Ruby 1.9.3 and 2.0.0.
|
15
|
+
|
16
|
+
Released under the three-clause BSD open source license.
|
17
|
+
http://opensource.org/licenses/BSD-3-Clause
|
18
|
+
See the LICENSE file.
|
19
|
+
|
20
|
+
Usage
|
21
|
+
===
|
22
|
+
You can either use the TaskManager module to make procedural calls, or
|
23
|
+
instantiate a BatchManager object. BatchManager objects have a cleaner API,
|
24
|
+
if you have tasks which are all processed in the same scope.
|
25
|
+
|
26
|
+
Using a BatchManager
|
27
|
+
---
|
28
|
+
```ruby
|
29
|
+
taskname = 'db-insert' # can be any valid hash key: string, symbol, etc.
|
30
|
+
duration = 15 # batch duration of 15 seconds
|
31
|
+
callback = lambda { |result| print "The return value was #{result}.\n" }
|
32
|
+
|
33
|
+
mgr = TaskBatcher::BatchManager.new(taskname, callback, duration) do |tasks|
|
34
|
+
# This batcher performs the operation of inserting rows into a DB.
|
35
|
+
# This is an example of how to aggregate processing of many tasks.
|
36
|
+
sql = "INSERT INTO pet_owners VALUES ("
|
37
|
+
tasks.each do |task|
|
38
|
+
# each +task+ is the params hash from a single call to #task
|
39
|
+
sql += "( #{task[:name]}, #{task[:pet]} ), "
|
40
|
+
end
|
41
|
+
sql += ")\n"
|
42
|
+
result = (execute that SQL) # +result+ will be available to a callback
|
43
|
+
end
|
44
|
+
|
45
|
+
mgr.task name: 'Alice', pet: 'moa'
|
46
|
+
mgr.task name: 'Bob', pet: 'cassowary'
|
47
|
+
# ... etc. ...
|
48
|
+
```
|
49
|
+
|
50
|
+
Using the TaskBatcher module
|
51
|
+
---
|
52
|
+
```ruby
|
53
|
+
taskname = 'db-insert' # can be any valid hash key: string, symbol, etc.
|
54
|
+
TaskBatcher.set_batch_duration(taskname, 15)
|
55
|
+
callback = lambda {|retval| print "The return value was #{retval}\n"}
|
56
|
+
|
57
|
+
def db_insert(data_list)
|
58
|
+
sql = "INSERT INTO pet_owners VALUES ("
|
59
|
+
data_list.each do |data|
|
60
|
+
# each +data+ row is the params hash from a single call to #task
|
61
|
+
sql += "( #{data[:name]}, #{data[:pet]} ), "
|
62
|
+
end
|
63
|
+
sql += ")\n"
|
64
|
+
retval = (execute that SQL) # retval will be available to a callback
|
65
|
+
end
|
66
|
+
|
67
|
+
pet_owner_1 = {name: 'Alice', pet: 'moa'}
|
68
|
+
TaskBatcher.task(taskname, pet_owner_1, callback) do |tasks|
|
69
|
+
db_insert(tasks)
|
70
|
+
end
|
71
|
+
|
72
|
+
pet_owner_2 = {name: 'Bob', pet: 'cassowary'}
|
73
|
+
TaskBatcher.task(taskname, pet_owner_2, callback) do |tasks|
|
74
|
+
db_insert(tasks)
|
75
|
+
end
|
76
|
+
... etc. ...
|
77
|
+
```
|
78
|
+
|
79
|
+
Setting batch durations
|
80
|
+
===
|
81
|
+
```ruby
|
82
|
+
TaskBatcher.default_batch_duration # returns 60, the initial default
|
83
|
+
|
84
|
+
mytask = 'task name 1'
|
85
|
+
TaskBatcher.set_batch_duration(mytask, 120) # 2 minutes
|
86
|
+
TaskBatcher.batch_duration(mytask) # returns 120
|
87
|
+
TaskBatcher.batch_duration('your task') # returns 60, the default
|
88
|
+
|
89
|
+
TaskBatcher.set_default_batch_duration(30)
|
90
|
+
TaskBatcher.batch_duration('another task') # returns 30
|
91
|
+
TaskBatcher.batch_duration('your task') # returns 30 -- default changed
|
92
|
+
TaskBatcher.batch_duration(mytask) # still returns 120
|
93
|
+
```
|
94
|
+
|
95
|
+
Notes
|
96
|
+
===
|
97
|
+
* Batches are grouped by +taskname+. ('db-insert' in the first example.)
|
98
|
+
* If no batch duration is given, the default batch duration is used. The
|
99
|
+
default batch duration is initially 60 seconds, but clients can change
|
100
|
+
the default.
|
101
|
+
* Batch parameters may be of any type, though hashes seem an obvious choice.
|
102
|
+
The batched function block must accept a data-list, where a single
|
103
|
+
data-item constitutes the parameters of a single call within the batch.
|
104
|
+
* The batched function block can return any data type. If a callback is
|
105
|
+
provided, it must accept the data type returned by the block. A
|
106
|
+
callback value of nil indicates that the return value may be discarded.
|
107
|
+
* TaskBatcher uses Event Machine. Event-driven programming is tricky, and
|
108
|
+
Event Machine is complex on top of that. Due to fundamental
|
109
|
+
limitations, TaskBatcher can only guarantee that batches will be
|
110
|
+
processed after a delay of *at least* the batch duration.
|
111
|
+
* Since Ruby's threading has limitations, TaskBatcher gives best performance
|
112
|
+
if most/all of the client code is event-driven and uses Event Machine.
|
113
|
+
|
114
|
+
References:
|
115
|
+
===
|
116
|
+
* Event Machine: http://everburning.com/wp-content/uploads/2009/02/eventmachine_introduction_10.pdf
|
117
|
+
|
118
|
+
* Batchers: http://www.youtube.com/watch?v=EIyixC9NsLI
|
data/lib/task_batcher.rb
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
# TaskBatcher allows clients to trigger jobs (such as database writes, e.g.)
|
2
|
+
# which are more efficiently processed in batches, but which should be processed
|
3
|
+
# reasonably quickly in any case. Each batch has a batch duration. Initially,
|
4
|
+
# invoking #task starts a batch and adds the initial task's parameters, and
|
5
|
+
# also starts a timer. Until the batch duration expires, any other calls to
|
6
|
+
# #task (with the same task name) will add the new call's parameters to the
|
7
|
+
# batch's collection. When the batch duration expires, the block processes the
|
8
|
+
# list of parameters.
|
9
|
+
#
|
10
|
+
# A procedural API (in the TaskBatcher module) and a BatchManager object are
|
11
|
+
# both provided. The BatchManager has a cleaner API, if your tasks are all
|
12
|
+
# processed within the same scope.
|
13
|
+
#
|
14
|
+
# Example usage:
|
15
|
+
#
|
16
|
+
# * BatchManager (OO) API:
|
17
|
+
# =============================================================================
|
18
|
+
# taskname = 'db-insert' # can be any valid hash key: string, symbol, etc.
|
19
|
+
# duration = 15 # batch duration of 15 seconds
|
20
|
+
# callback = lambda { |result| print "The return value was #{result}.\n" }
|
21
|
+
#
|
22
|
+
# mgr = TaskBatcher::BatchManager.new(taskname, callback, duration) do |tasks|
|
23
|
+
# # This batcher performs the operation of inserting rows into a DB.
|
24
|
+
# # This is an example of how to aggregate processing of many tasks.
|
25
|
+
# sql = "INSERT INTO pet_owners VALUES ("
|
26
|
+
# tasks.each do |task|
|
27
|
+
# # each +task+ is the params hash from a single call to #task
|
28
|
+
# sql += "( #{task[:name]}, #{task[:pet]} ), "
|
29
|
+
# end
|
30
|
+
# sql += ")\n"
|
31
|
+
# result = (execute that SQL) # +result+ will be available to a callback
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# mgr.task name: 'Alice', pet: 'moa'
|
35
|
+
# mgr.task name: 'Bob', pet: 'cassowary'
|
36
|
+
# # ... etc. ...
|
37
|
+
# =============================================================================
|
38
|
+
#
|
39
|
+
# * Procedural TaskBatcher API:
|
40
|
+
# =============================================================================
|
41
|
+
# taskname = 'db-insert' # can be any valid hash key: string, symbol, etc.
|
42
|
+
# callback = lambda {|retval| print "The return value was #{retval}\n"}
|
43
|
+
# TaskBatcher.set_batch_duration(taskname, 15)
|
44
|
+
#
|
45
|
+
# def db_insert(data_list)
|
46
|
+
# sql = "INSERT INTO pet_owners VALUES ("
|
47
|
+
# data_list.each do |data|
|
48
|
+
# # each +data+ row is the params hash from a single call to #task
|
49
|
+
# sql += "( #{data[:name]}, #{data[:pet]} ), "
|
50
|
+
# end
|
51
|
+
# sql += ")\n"
|
52
|
+
# retval = (execute that SQL) # retval will be available to a callback
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# pet_owner_1 = {name: 'Alice', pet: 'moa'}
|
56
|
+
# TaskBatcher.task(taskname, pet_owner_1, callback) do |tasks|
|
57
|
+
# db_insert(tasks)
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# pet_owner_2 = {name: 'Bob', pet: 'cassowary'}
|
61
|
+
# TaskBatcher.task(taskname, pet_owner_2, callback) do |tasks|
|
62
|
+
# db_insert(tasks)
|
63
|
+
# end
|
64
|
+
# ... etc. ...
|
65
|
+
# =============================================================================
|
66
|
+
#
|
67
|
+
# Notes:
|
68
|
+
# * Batches are grouped by +taskname+. ('db-insert' in the example above.)
|
69
|
+
# * If no batch duration is given, the default batch duration is used. The
|
70
|
+
# default batch duration is initially 60 seconds, but clients can change
|
71
|
+
# the default.
|
72
|
+
# * Batch parameters may be of any type, though hashes seem an obvious choice.
|
73
|
+
# The batched function block must accept a data-list, where a single
|
74
|
+
# data-item constitutes the parameters of a single call within the batch.
|
75
|
+
# * The batched function block can return any data type. If a callback is
|
76
|
+
# provided, it must accept the data type returned by the block. A
|
77
|
+
# callback value of nil indicates that the return value may be discarded.
|
78
|
+
# * TaskBatcher uses Event Machine. Event-driven programming is tricky, and
|
79
|
+
# Event Machine is complex on top of that. Due to fundamental
|
80
|
+
# limitations, TaskBatcher can only guarantee that batches will be
|
81
|
+
# processed after a delay of *at least* the batch duration.
|
82
|
+
# * Since Ruby's threading has limitations, TaskBatcher gives best performance
|
83
|
+
# if most/all of the client code is event-driven and uses Event Machine.
|
84
|
+
#
|
85
|
+
# References:
|
86
|
+
# * Event Machine: http://everburning.com/wp-content/uploads/2009/02/eventmachine_introduction_10.pdf
|
87
|
+
#
|
88
|
+
|
89
|
+
require 'eventmachine'
|
90
|
+
|
91
|
+
module TaskBatcher
|
92
|
+
class Error < ::StandardError; end
|
93
|
+
|
94
|
+
# Clients can set the default batch duration. If client does not set the
|
95
|
+
# default batch duration, it has the following value.
|
96
|
+
METADEFAULT_DURATION = 60 # in seconds
|
97
|
+
private_constant :METADEFAULT_DURATION
|
98
|
+
|
99
|
+
# Reset all state; forget everything about batches in progress, tasknames,
|
100
|
+
# customized durations, etc.
|
101
|
+
def reset
|
102
|
+
@default_batch_duration = @batch_durations = @batches = @mutex = nil
|
103
|
+
|
104
|
+
# Other system components might be using EM. If an EM reactor is running,
|
105
|
+
# do nothing. If not, start one on a thread that we'll own.
|
106
|
+
if not EM.reactor_running?
|
107
|
+
@em_thread = Thread.new { EM.run }
|
108
|
+
while not EM.reactor_running?; sleep 0.01; end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# With no guidance from the client, the default batch duration is the value of
|
113
|
+
# METADEFAULT_DURATION. However, in addition to being able to set the batch
|
114
|
+
# duration of any taskname, clients can set the global default batch duration.
|
115
|
+
#
|
116
|
+
# @param seconds [Fixnum] default batch duration in seconds
|
117
|
+
def set_default_batch_duration(seconds)
|
118
|
+
@default_batch_duration = seconds
|
119
|
+
end
|
120
|
+
|
121
|
+
# Return the default batch duration in seconds. (This will be the value of
|
122
|
+
# METADEFAULT_DURATION, unless you have called #set_default_batch_duration.)
|
123
|
+
#
|
124
|
+
# @return [Fixnum] the default batch duration in seconds
|
125
|
+
def default_batch_duration
|
126
|
+
@default_batch_duration || METADEFAULT_DURATION
|
127
|
+
end
|
128
|
+
|
129
|
+
# Set the batch duration for a particular task name.
|
130
|
+
#
|
131
|
+
# @param taskname [String,Symbol] taskname for which we set the batch duration
|
132
|
+
# @param seconds [Fixnum] number of seconds to delay before running the batch
|
133
|
+
def set_batch_duration(taskname, seconds)
|
134
|
+
durations[taskname] = seconds
|
135
|
+
end
|
136
|
+
|
137
|
+
# Return the batch duration for a particular task name.
|
138
|
+
#
|
139
|
+
# @return [Fixnum] number of seconds to delay before running this batch
|
140
|
+
def batch_duration(taskname)
|
141
|
+
durations[taskname] || default_batch_duration
|
142
|
+
end
|
143
|
+
|
144
|
+
# Add a task to a batch, or start a new batch if none exists with this
|
145
|
+
# taskname.
|
146
|
+
#
|
147
|
+
# Once the batch duration expires, all tasks in the batch will be run, i.e.
|
148
|
+
# the block will be passed a list of task_args_hash values and the block is
|
149
|
+
# expected to process that entire list.
|
150
|
+
#
|
151
|
+
# Usage:
|
152
|
+
# TaskBatcher.task('taskname', {var1: 'foo', var2: 'bar'}) do |args_hashes|
|
153
|
+
# var1s = args_hashes.map { |h| h[:var1] }
|
154
|
+
# var2s = args_hashes.map { |h| h[:var2] }
|
155
|
+
# print "This batcher was invoked with #{args_hashes.count} tasks.\n"
|
156
|
+
# print "Hopefully the tasks can be batched efficiently.\n"
|
157
|
+
# print "Then it would be faster to invoke 'func1(var1s); func2(var2s)' \n"
|
158
|
+
# print "instead of 'args_hashes.each { |h| func12(h[:var1], h[:var2]) }'\n"
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# Only the batch's FIRST callback and block are respected!
|
162
|
+
# # assuming the batch is empty here...
|
163
|
+
#
|
164
|
+
# TaskBatcher.task('mytask', args1, callback1) { func1 }
|
165
|
+
# TaskBatcher.task('mytask', args2, callback2) { func2 }
|
166
|
+
# TaskBatcher.task('mytask', args3, callback3) { func3 }
|
167
|
+
#
|
168
|
+
# # ... then this batch will invoke func1([args1, args2, args3]) and invoke
|
169
|
+
# # callback1 on the result. func2/func3 and callback2/callback3 will
|
170
|
+
# # never be used!
|
171
|
+
#
|
172
|
+
def task(taskname, task_args_hash, callback=nil, &block)
|
173
|
+
raise Error, 'taskname required' if taskname.nil?
|
174
|
+
mutex.synchronize do
|
175
|
+
this_batch = batches[taskname]
|
176
|
+
if this_batch.nil?
|
177
|
+
batches[taskname] = {
|
178
|
+
block: block,
|
179
|
+
callback: callback,
|
180
|
+
args_hashes: [ task_args_hash ],
|
181
|
+
}
|
182
|
+
EM.add_timer(batch_duration(taskname)) do
|
183
|
+
EM.defer { process_batch(taskname) }
|
184
|
+
end
|
185
|
+
else
|
186
|
+
this_batch[:args_hashes] << task_args_hash
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Generally, the #process_batch method will be called by Event Machine when
|
192
|
+
# the batch duration expires. However, it is public because the caller may
|
193
|
+
# want to explicitly process a batch.
|
194
|
+
#
|
195
|
+
# @param taskname [String,Symbol] the name of the batch to process
|
196
|
+
def process_batch(taskname)
|
197
|
+
this_batch = nil
|
198
|
+
|
199
|
+
# Grab the batch. If another TaskBatcher call is added immediately after
|
200
|
+
# this synchronize step, then it just starts a new batch.
|
201
|
+
mutex.synchronize do
|
202
|
+
this_batch = batches[taskname]
|
203
|
+
batches[taskname] = nil
|
204
|
+
end
|
205
|
+
return nil if this_batch.nil?
|
206
|
+
|
207
|
+
# Grab the block, list of batch args, and callback. Then run the batch!
|
208
|
+
batch_processor = this_batch[:block]
|
209
|
+
args_hashes = this_batch[:args_hashes]
|
210
|
+
callback = this_batch[:callback] || lambda {|arg_hashes| nil}
|
211
|
+
begin
|
212
|
+
results = batch_processor[args_hashes]
|
213
|
+
rescue Exception => e
|
214
|
+
raise Error, "Exception raised during TaskBatcher processing:\n" +
|
215
|
+
"#{e.backtrace.join("\n ")}"
|
216
|
+
end
|
217
|
+
|
218
|
+
# Run the callback. The callback's return value isn't accessible in any
|
219
|
+
# meaningful way.
|
220
|
+
callback[results]
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def mutex; @mutex ||= Mutex.new; end
|
227
|
+
def batches; @batches ||= {}; end
|
228
|
+
def durations; @batch_durations ||= {}; end
|
229
|
+
|
230
|
+
extend self
|
231
|
+
reset
|
232
|
+
end
|
233
|
+
|
234
|
+
# The BatchManager class provides a more OO-style API to the functionality of
|
235
|
+
# TaskBatcher. Multiple BatchManagers can be instantiated, and there is no
|
236
|
+
# need to repeat the callback and block parameters multiple times in your
|
237
|
+
# code.
|
238
|
+
#
|
239
|
+
# This is a cleaner interface if your batched jobs get added in a reasonably
|
240
|
+
# localized scope (where your BatchManager can live). If batched jobs get
|
241
|
+
# added all over the code base, the TaskBatcher interface is probably a better
|
242
|
+
# fit, even though you'll have to repeat the callback and block every time
|
243
|
+
# you add a job.
|
244
|
+
#
|
245
|
+
class BatchManager
|
246
|
+
|
247
|
+
# Create a BatchManager to wrap calls to TaskBatcher.
|
248
|
+
#
|
249
|
+
# @param name [String,Symbol] arbitrary task identifier, or nil to
|
250
|
+
# auto-generate one
|
251
|
+
# @param callback [#call] callable which takes one parameter (of whatever
|
252
|
+
# type is returned by the block), or nil to discard block's return value
|
253
|
+
# @param duration [Fixnum] batch duration, or nil to use the default
|
254
|
+
# @param block [#call] callable which takes one parameter (list of
|
255
|
+
# parameter-groups, one per #add invocation) and batch-processes them
|
256
|
+
def initialize(name=nil, callback=nil, duration=nil, &block)
|
257
|
+
@name = name || "ObjectID:#{self.object_id}"
|
258
|
+
@callback = callback
|
259
|
+
@block = block
|
260
|
+
(self.batch_duration = duration) if duration
|
261
|
+
end
|
262
|
+
|
263
|
+
# Add a job to the TaskBatch, to be executed when the batch duration expires.
|
264
|
+
#
|
265
|
+
# Once the batch duration expires, all tasks in the batch will be run, i.e.
|
266
|
+
# the block will be passed a list of task_args_hash values and the block is
|
267
|
+
# expected to process that entire list.
|
268
|
+
#
|
269
|
+
# Usage:
|
270
|
+
# batch = TaskBatch.new('mybatch', mycallback) {|data_list| myfn(data_list)}
|
271
|
+
# batch.task(params1)
|
272
|
+
# batch.task(params2)
|
273
|
+
# ...
|
274
|
+
# # after the batch duration expires, any params in the batch will be
|
275
|
+
# # processed all at once
|
276
|
+
#
|
277
|
+
def task(task_params)
|
278
|
+
TaskBatcher.task(@name, task_params, @callback, &@block)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Set the batch duration for this TaskBatch.
|
282
|
+
#
|
283
|
+
# @param seconds [Fixnum] number of seconds to delay before running the batch
|
284
|
+
def batch_duration=(seconds)
|
285
|
+
TaskBatcher.set_batch_duration(@name, seconds)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Return the batch duration for this TaskBatch
|
289
|
+
#
|
290
|
+
# @return [Fixnum] number of seconds to delay before running this batch
|
291
|
+
def batch_duration
|
292
|
+
TaskBatcher.batch_duration(@name)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Generally, the #process_batch method will be called by Event Machine when
|
296
|
+
# the batch duration expires. However, this method is public because the
|
297
|
+
# caller may want to explicitly process a batch.
|
298
|
+
def process_batch
|
299
|
+
TaskBatcher.process_batch(@name)
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = %q{task_batcher}
|
4
|
+
s.version = '0.1.0'
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
s.required_ruby_version = '>= 1.9.3'
|
7
|
+
|
8
|
+
if s.respond_to? :required_rubygems_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2")
|
10
|
+
end
|
11
|
+
s.authors = ['Jon Snitow']
|
12
|
+
s.email = ['opensource@verticalbrands.com']
|
13
|
+
s.date = '2013-04-09'
|
14
|
+
s.summary = 'Dynamically add tasks to batches, process batches after a time'
|
15
|
+
s.description = <<-EOT
|
16
|
+
Some tasks, like database inserts, are much more efficient to process in a
|
17
|
+
batch. However, we generally want our tasks to be processed "soon" even if
|
18
|
+
there's only one task. The TaskBatcher gem groups tasks by a taskname
|
19
|
+
parameter, and starts a timer when the first task comes in. After the batch
|
20
|
+
timer expires, it processes all tasks that it received in that time. (The
|
21
|
+
caller provides the block to process the tasks.)
|
22
|
+
|
23
|
+
Uses EventMachine under the hood. May be combined with Messenger for
|
24
|
+
durability guarantees.
|
25
|
+
EOT
|
26
|
+
s.files = Dir[
|
27
|
+
'{lib}/**/*.rb',
|
28
|
+
'LICENSE',
|
29
|
+
'*.md',
|
30
|
+
'task_batcher.gemspec',
|
31
|
+
]
|
32
|
+
s.require_paths = ['lib']
|
33
|
+
|
34
|
+
s.rubyforge_project = 'task_batcher'
|
35
|
+
s.rubygems_version = '>= 1.8.6'
|
36
|
+
s.homepage = 'http://github.com/apartmentlist'
|
37
|
+
|
38
|
+
s.add_dependency('eventmachine')
|
39
|
+
s.add_development_dependency('rspec')
|
40
|
+
s.add_development_dependency('mocha')
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: task_batcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jon Snitow
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: mocha
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: ! 'Some tasks, like database inserts, are much more efficient to process
|
63
|
+
in a
|
64
|
+
|
65
|
+
batch. However, we generally want our tasks to be processed "soon" even if
|
66
|
+
|
67
|
+
there''s only one task. The TaskBatcher gem groups tasks by a taskname
|
68
|
+
|
69
|
+
parameter, and starts a timer when the first task comes in. After the batch
|
70
|
+
|
71
|
+
timer expires, it processes all tasks that it received in that time. (The
|
72
|
+
|
73
|
+
caller provides the block to process the tasks.)
|
74
|
+
|
75
|
+
|
76
|
+
Uses EventMachine under the hood. May be combined with Messenger for
|
77
|
+
|
78
|
+
durability guarantees.
|
79
|
+
|
80
|
+
'
|
81
|
+
email:
|
82
|
+
- opensource@verticalbrands.com
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- lib/task_batcher.rb
|
88
|
+
- LICENSE
|
89
|
+
- README.md
|
90
|
+
- task_batcher.gemspec
|
91
|
+
homepage: http://github.com/apartmentlist
|
92
|
+
licenses: []
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 1.9.3
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ! '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '1.2'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project: task_batcher
|
111
|
+
rubygems_version: 1.8.19
|
112
|
+
signing_key:
|
113
|
+
specification_version: 3
|
114
|
+
summary: Dynamically add tasks to batches, process batches after a time
|
115
|
+
test_files: []
|
116
|
+
has_rdoc:
|