task_batcher 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +30 -0
  2. data/README.md +118 -0
  3. data/lib/task_batcher.rb +301 -0
  4. data/task_batcher.gemspec +41 -0
  5. 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.
@@ -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
@@ -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: