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.
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: