task_batcher 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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:
|