resque-heroku-scaler 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md ADDED
@@ -0,0 +1,6 @@
1
+ ## 0.2.1 (2011-11-27)
2
+
3
+ * Separate scaler process from worker hooks to prevent race conditions during worker shutdown
4
+ * Scale manager for Heroku Cedar platform
5
+ * Scale manager for local workers using rush
6
+ * Core test coverage
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) Aaron Dunnington
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ Resque Heroku Scaler
2
+ ====================
3
+
4
+ Provides autoscaling for [Resque][rq] workers on [Heroku][hk]. Based on
5
+ previous scaling work developed by [Daniel Huckstep][dh] and
6
+ [Alexander Murmann][am].
7
+
8
+ Autoscaling behavior is provided through a separate monitor process. The
9
+ scaler monitor process polls for pending jobs against the specified Resque
10
+ Redis backend at a configurable interval. The scaler process runs as a worker
11
+ process on Heroku.
12
+
13
+ ##Setup
14
+
15
+ Add the following environment variables to your Heroku environment:
16
+
17
+ * HEROKU_APP
18
+ * HEROKU_USERNAME
19
+ * HEROKU_PASSWORD
20
+
21
+ In your Procfile, configure the scaler as a worker process using:
22
+
23
+ ```
24
+ scaler: bundle exec rake resque:scaler:run
25
+ ```
26
+
27
+ To run the scaler process, use the following command. Note, the scaler process
28
+ is intended to run as a single instance.
29
+
30
+ ```
31
+ heroku scale scaler=1
32
+ ```
33
+
34
+ In your development environment, the scaler process can run local worker
35
+ processes using the rush library. To configure, use the following in
36
+ an initializer.
37
+
38
+ ```ruby
39
+ require 'resque/plugins/resque-heroku-scaler'
40
+
41
+ if Rails.env.development?
42
+ ENV["RUSH_PATH"] ||= File.expand_path('/path/to/app', __FILE__)
43
+ Resque::Plugins::ResqueHerokuScaler.configure do |c|
44
+ c.scale_manager = :local
45
+ end
46
+ end
47
+ ```
48
+
49
+ [rq]: http://github.com/defunkt/resque
50
+ [hk]: http://devcenter.heroku.com/articles/cedar
51
+ [dh]: http://verboselogging.com/2010/07/30/auto-scale-your-resque-workers-on-heroku
52
+ [am]: https://github.com/ajmurmann/resque-heroku-autoscaler
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.test_files = FileList['test/*_test.rb']
8
+ t.verbose = true
9
+ end
@@ -0,0 +1,6 @@
1
+ require 'resque/plugins/resque_heroku_scaler/version'
2
+ require 'resque/plugins/resque_heroku_scaler/config'
3
+ require 'resque/plugins/resque_heroku_scaler/manager'
4
+ require 'resque/plugins/resque_heroku_scaler/worker'
5
+ require 'resque/plugins/resque_heroku_scaler/resque'
6
+ require 'resque/plugins/resque_heroku_scaler'
@@ -0,0 +1,71 @@
1
+ module Resque
2
+ module Plugins
3
+ module ResqueHerokuScaler
4
+ module Config
5
+ extend self
6
+
7
+ attr_writer :scale_manager
8
+ attr_writer :scale_interval
9
+ attr_writer :poll_interval
10
+ attr_writer :scale_timeout
11
+ attr_reader :scale_with
12
+
13
+ def scale_manager
14
+ @scale_manager || :heroku
15
+ end
16
+
17
+ def scale_interval
18
+ @scale_interval || 60
19
+ end
20
+
21
+ def poll_interval
22
+ @poll_interval || 5
23
+ end
24
+
25
+ def scale_timeout
26
+ @scale_timeout || 90
27
+ end
28
+
29
+ def scale_for(pending)
30
+ return @scale_with.call(pending) if @scale_with
31
+ default_scale_with(pending)
32
+ end
33
+
34
+ def scale_with=(block)
35
+ @scale_with = block
36
+ end
37
+
38
+ def default_scale_with(pending)
39
+ return 0 if pending <= 0
40
+
41
+ [{
42
+ :workers => 1,
43
+ :jobs => 1
44
+ },
45
+ {
46
+ :workers => 2,
47
+ :jobs => 15
48
+ },
49
+ {
50
+ :workers => 3,
51
+ :jobs => 25
52
+ },
53
+ {
54
+ :workers => 4,
55
+ :jobs => 40
56
+ },
57
+ {
58
+ :workers => 5,
59
+ :jobs => 60
60
+ }
61
+ ].reverse_each do |required_scale|
62
+ if pending >= required_scale[:jobs]
63
+ return required_scale[:workers]
64
+ end
65
+ end
66
+ return 0
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,25 @@
1
+ require 'heroku'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module ResqueHerokuScaler
6
+ module Manager
7
+
8
+ class Heroku
9
+ def initialize(options={})
10
+ @heroku = ::Heroku::Client.new(ENV['HEROKU_USERNAME'], ENV['HEROKU_PASSWORD'])
11
+ end
12
+
13
+ def workers
14
+ @heroku.ps(ENV['HEROKU_APP']).count { |p| p["process"] =~ /worker\.\d?/ }
15
+ end
16
+
17
+ def workers=(qty)
18
+ @heroku.ps_scale(ENV['HEROKU_APP'], :type => 'worker', :qty => qty)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ require 'rush'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module ResqueHerokuScaler
6
+ module Manager
7
+
8
+ class Local
9
+
10
+ def initialize(options={})
11
+ @path = options[:path] || ENV['RUSH_PATH']
12
+ @processes = []
13
+ end
14
+
15
+ def workers
16
+ @processes.length
17
+ end
18
+
19
+ def workers=(qty)
20
+ active = workers
21
+ return if qty == active
22
+ if qty > active
23
+ scale_up(qty-active)
24
+ return
25
+ end
26
+ scale_down(active-qty)
27
+ end
28
+
29
+ def scale_up(qty)
30
+ qty.times do
31
+ process = Rush::Box.new[@path].bash('rake resque:work', :background => true, :env => { :BUNDLE_GEMFILE => '' })
32
+ @processes.push(process) if process
33
+ end
34
+ end
35
+
36
+ def scale_down(qty)
37
+ i = 0
38
+ until i == qty or @processes.empty?
39
+ process = @processes.pop
40
+ kill(process)
41
+ i += 1
42
+ end
43
+ end
44
+
45
+ def kill(process)
46
+ process.children.each do |child|
47
+ kill(child)
48
+ end
49
+ process.kill
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ module Resque
2
+ module Plugins
3
+ module ResqueHerokuScaler
4
+ module Manager
5
+ extend self
6
+
7
+ def instance
8
+ @@instance ||= init_manager
9
+ end
10
+
11
+ def init_manager
12
+ handler = Resque::Plugins::ResqueHerokuScaler::Config.scale_manager
13
+ return handler unless [Symbol, Array, String].include? handler.class
14
+
15
+ options = {}
16
+ handler, options = handler if handler.is_a?(Array)
17
+ require File.dirname(__FILE__) + "/manager/#{handler}"
18
+ const_get(handler.to_s.capitalize).new(options)
19
+ end
20
+
21
+ def method_missing(m, *args)
22
+ instance.send(m, *args)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ module Resque
2
+
3
+ alias_method :original_info, :info
4
+
5
+ def scaling
6
+ Worker.scaling
7
+ end
8
+
9
+ def info
10
+ info_with_scale = original_info
11
+ info_with_scale[:scaling] = scaling.size
12
+ return info_with_scale
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ namespace :resque do
2
+ namespace :scaler do
3
+ task :setup
4
+
5
+ desc "Start Resque Heroku Scaler process"
6
+ task :run => :setup do
7
+ require 'resque/plugins/resque-heroku-scaler'
8
+ Resque::Plugins::ResqueHerokuScaler.run
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Resque
2
+ module Plugins
3
+ module ResqueHerokuScaler
4
+ Version = VERSION = "0.2.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,85 @@
1
+ module Resque
2
+
3
+ class Worker
4
+ alias_method :original_unregister_worker, :unregister_worker
5
+
6
+ def work(interval = 5.0, &block)
7
+ interval = Float(interval)
8
+ $0 = "resque: Starting"
9
+ startup
10
+
11
+ loop do
12
+ wait_for_scale if scaling?
13
+ break if shutdown?
14
+
15
+ if not paused? and job = reserve
16
+ log "got: #{job.inspect}"
17
+ job.worker = self
18
+ run_hook :before_fork, job
19
+ working_on job
20
+
21
+ if @child = fork
22
+ srand # Reseeding
23
+ procline "Forked #{@child} at #{Time.now.to_i}"
24
+ Process.wait(@child)
25
+ else
26
+ procline "Processing #{job.queue} since #{Time.now.to_i}"
27
+ perform(job, &block)
28
+ exit! unless @cant_fork
29
+ end
30
+
31
+ done_working
32
+ @child = nil
33
+ else
34
+ break if interval.zero?
35
+ log! "Sleeping for #{interval} seconds"
36
+ procline paused? ? "Paused" : "Waiting for #{@queues.join(',')}"
37
+ sleep interval
38
+ end
39
+ end
40
+
41
+ ensure
42
+ unregister_worker
43
+ end
44
+
45
+ def wait_for_scale
46
+ redis.set("scale:#{self}", true)
47
+ sleep 1 while scaling? and not shutdown?
48
+ redis.del("scale:#{self}")
49
+ end
50
+
51
+ def unregister_worker
52
+ redis.del("scale:#{self}")
53
+ original_unregister_worker
54
+ end
55
+
56
+ def scaling?
57
+ redis.exists(:scale)
58
+ end
59
+
60
+ def self.scaling
61
+ names = all
62
+ return [] unless names.any?
63
+
64
+ names.map! { |name| "scale:#{name}" }
65
+
66
+ reportedly_scaling = {}
67
+
68
+ begin
69
+ reportedly_scaling = redis.mapped_mget(*names).reject do |key, value|
70
+ value.nil? || value.empty?
71
+ end
72
+ rescue Redis::Distributed::CannotDistribute
73
+ names.each do |name|
74
+ value = redis.get name
75
+ reportedly_scaling[name] = value unless value.nil? || value.empty?
76
+ end
77
+ end
78
+
79
+ reportedly_scaling.keys.map do |key|
80
+ find key.sub("scale:", '')
81
+ end.compact
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,106 @@
1
+ module Resque
2
+ module Plugins
3
+
4
+ module ResqueHerokuScaler
5
+ class << self
6
+
7
+ def run
8
+ startup
9
+ loop do
10
+ begin
11
+ scale
12
+ rescue Exception => e
13
+ log "Scale failed with #{e.class.name} #{e.message}"
14
+ end
15
+ wait_for_scale
16
+ end
17
+ end
18
+
19
+ def scale
20
+ required = scale_for(pending)
21
+ active = workers
22
+
23
+ return if required == active
24
+
25
+ log "Scale workers from #{active} to #{required}"
26
+
27
+ if required > active
28
+ scale_workers(required)
29
+ return
30
+ end
31
+
32
+ signal_workers
33
+ stop = timeout
34
+ wait_for_workers until ready_to_scale(active) or timeout?(stop)
35
+ scale_workers(required)
36
+
37
+ ensure
38
+ resume_workers
39
+ end
40
+
41
+ def wait_for_scale
42
+ sleep Resque::Plugins::ResqueHerokuScaler::Config.scale_interval
43
+ end
44
+
45
+ def wait_for_workers
46
+ sleep Resque::Plugins::ResqueHerokuScaler::Config.poll_interval
47
+ end
48
+
49
+ def scale_for(pending)
50
+ Resque::Plugins::ResqueHerokuScaler::Config.scale_for(pending)
51
+ end
52
+
53
+ def scale_workers(qty)
54
+ Resque::Plugins::ResqueHerokuScaler::Manager.workers = qty
55
+ end
56
+
57
+ def workers
58
+ Resque::Plugins::ResqueHerokuScaler::Manager.workers
59
+ end
60
+
61
+ def signal_workers
62
+ Resque.redis.set(:scale, true)
63
+ end
64
+
65
+ def resume_workers
66
+ Resque.redis.del(:scale)
67
+ end
68
+
69
+ def timeout?(stop)
70
+ Time.now >= stop
71
+ end
72
+
73
+ def timeout
74
+ Time.now + Resque::Plugins::ResqueHerokuScaler::Config.scale_timeout
75
+ end
76
+
77
+ def pending
78
+ Resque.info[:pending].to_i
79
+ end
80
+
81
+ def ready_to_scale(active)
82
+ Resque.info[:scaling] == active
83
+ end
84
+
85
+ def configure
86
+ yield Resque::Plugins::ResqueHerokuScaler::Config
87
+ end
88
+
89
+ def startup
90
+ STDOUT.sync = true
91
+ trap('TERM') do
92
+ log "Shutting down scaler"
93
+ exit
94
+ end
95
+ log "Starting scaler"
96
+ resume_workers
97
+ end
98
+
99
+ def log(message)
100
+ puts "*** #{message}"
101
+ end
102
+ end
103
+
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,61 @@
1
+ require 'test_helper'
2
+
3
+ class ConfigTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @config = Resque::Plugins::ResqueHerokuScaler::Config
6
+ end
7
+
8
+ def test_scale_manager_default
9
+ assert_equal :heroku, @config.scale_manager
10
+ end
11
+
12
+ def test_scale_interval_default
13
+ assert_equal 60, @config.scale_interval
14
+ end
15
+
16
+ def test_poll_interval_default
17
+ assert_equal 5, @config.poll_interval
18
+ end
19
+
20
+ def test_scale_timeout_default
21
+ assert_equal 90, @config.scale_timeout
22
+ end
23
+
24
+ def test_scale_for_default
25
+ assert_equal 2, @config.scale_for(20)
26
+ end
27
+
28
+ def test_scale_with_default
29
+ assert_equal nil, @config.scale_with
30
+ end
31
+
32
+ def test_custom_scale_with
33
+ @config.scale_with = Proc.new { |pending| 99 }
34
+ assert_equal 99, @config.scale_for(2)
35
+ @config.scale_with = nil
36
+ end
37
+
38
+ def test_scale_manager
39
+ @config.scale_manager = :local
40
+ assert_equal :local, @config.scale_manager
41
+ @config.scale_manager = :heroku
42
+ end
43
+
44
+ def test_scale_interval
45
+ @config.scale_interval = 99
46
+ assert_equal 99, @config.scale_interval
47
+ @config.scale_interval = 60
48
+ end
49
+
50
+ def test_poll_interval
51
+ @config.poll_interval = 99
52
+ assert_equal 99, @config.poll_interval
53
+ @config.poll_interval = 5
54
+ end
55
+
56
+ def test_scale_timeout
57
+ @config.scale_timeout = 99
58
+ assert_equal 99, @config.scale_timeout
59
+ @config.scale_timeout = 90
60
+ end
61
+ end
@@ -0,0 +1,68 @@
1
+ require 'test_helper'
2
+
3
+ class ResqueHerokuScalerTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @scaler = Resque::Plugins::ResqueHerokuScaler
6
+ @config = Resque::Plugins::ResqueHerokuScaler::Config
7
+
8
+ @redis = mock('Mock Redis')
9
+ @redis.stubs(:set).with(:scale, true).returns(true)
10
+ @redis.stubs(:del).with(:scale).returns(true)
11
+ Resque.stubs(:redis).returns(@redis)
12
+
13
+ @manager = mock('Mock Manager')
14
+ Resque::Plugins::ResqueHerokuScaler::Manager.stubs(:instance).returns(@manager)
15
+ end
16
+
17
+ def test_no_scale_for_zero_jobs
18
+ Resque.stubs(:info).returns({ :pending => 0, :scaling => 0 })
19
+ @manager.expects(:workers).returns(0)
20
+ @manager.expects(:workers=).never
21
+ @scaler.scale()
22
+ end
23
+
24
+ def test_scale_up_for_pending_job
25
+ Resque.stubs(:info).returns({ :pending => 1, :scaling => 0 })
26
+ @manager.expects(:workers).returns(0)
27
+ @manager.expects(:workers=).with(1)
28
+ @scaler.scale()
29
+ end
30
+
31
+ def test_scale_down_timeout
32
+ @config.scale_timeout = 1
33
+ Resque.stubs(:info).returns({ :pending => 0, :scaling => 0 })
34
+ @manager.expects(:workers).returns(1)
35
+ @manager.expects(:workers=).with(0)
36
+ @scaler.scale()
37
+ @config.scale_timeout = 90
38
+ end
39
+
40
+ def test_scale_down_for_zero_jobs
41
+ Resque.stubs(:info).returns({ :pending => 0, :scaling => 1 })
42
+ @manager.expects(:workers).returns(1)
43
+ @manager.expects(:workers=).with(0)
44
+ @scaler.scale()
45
+ end
46
+
47
+ def test_configure
48
+ @scaler.configure do |c|
49
+ c.scale_manager = :local
50
+ c.scale_interval = 30
51
+ c.poll_interval = 10
52
+ c.scale_timeout = 20
53
+ c.scale_with = Proc.new { |pending| 99 }
54
+ end
55
+
56
+ assert_equal :local, @config.scale_manager
57
+ assert_equal 30, @config.scale_interval
58
+ assert_equal 10, @config.poll_interval
59
+ assert_equal 20, @config.scale_timeout
60
+ assert_equal 99, @config.scale_for(2)
61
+
62
+ @config.scale_manager = :heroku
63
+ @config.scale_interval = 60
64
+ @config.poll_interval = 5
65
+ @config.scale_timeout = 90
66
+ @config.scale_with = nil
67
+ end
68
+ end
@@ -0,0 +1,4 @@
1
+ require 'minitest/autorun'
2
+ require 'mocha'
3
+ require 'resque'
4
+ require 'resque/plugins/resque-heroku-scaler'
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class WorkerTest < MiniTest::Unit::TestCase
4
+ def test_wait_for_scale
5
+ Resque.redis.stubs(:exists).with(:scale).returns(true)
6
+ Resque.redis.expects(:set).with(regexp_matches(/^scale:(.)*$/), true)
7
+ Resque.redis.expects(:del).with(regexp_matches(/^scale:(.)*$/))
8
+
9
+ worker = Resque::Worker.new(['*'])
10
+ worker.stubs(:shutdown?).returns(true)
11
+ worker.stubs(:register_worker).returns(true)
12
+ worker.stubs(:unregister_worker).returns(true)
13
+ worker.work(0)
14
+ end
15
+
16
+ def test_unregister_worker
17
+ Resque.redis.expects(:del).with(regexp_matches(/^scale:(.)*$/))
18
+ Resque.redis.stubs(:del).with(regexp_matches(/^worker:(.)*$/))
19
+ Resque.redis.stubs(:del).with(regexp_matches(/^stat:(.)*$/))
20
+
21
+ worker = Resque::Worker.new(['*'])
22
+ worker.stubs(:shutdown?).returns(true)
23
+ worker.stubs(:register_worker).returns(true)
24
+ worker.work(0)
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-heroku-scaler
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.1
6
+ platform: ruby
7
+ authors:
8
+ - Aaron Dunnington
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-11-27 00:00:00 -05:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: resque
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: 1.19.0
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: heroku
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 2.14.0
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ description: " This gem provides autoscaling behavior for Resque jobs on Heroku.\n"
39
+ email: spirogh@gmail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - README.md
48
+ - Rakefile
49
+ - LICENSE
50
+ - HISTORY.md
51
+ - lib/resque/plugins/resque-heroku-scaler.rb
52
+ - lib/resque/plugins/resque_heroku_scaler/config.rb
53
+ - lib/resque/plugins/resque_heroku_scaler/manager/heroku.rb
54
+ - lib/resque/plugins/resque_heroku_scaler/manager/local.rb
55
+ - lib/resque/plugins/resque_heroku_scaler/manager.rb
56
+ - lib/resque/plugins/resque_heroku_scaler/resque.rb
57
+ - lib/resque/plugins/resque_heroku_scaler/tasks.rb
58
+ - lib/resque/plugins/resque_heroku_scaler/version.rb
59
+ - lib/resque/plugins/resque_heroku_scaler/worker.rb
60
+ - lib/resque/plugins/resque_heroku_scaler.rb
61
+ - test/config_test.rb
62
+ - test/resque_heroku_scaler_test.rb
63
+ - test/test_helper.rb
64
+ - test/worker_test.rb
65
+ has_rdoc: true
66
+ homepage: http://github.com/spiro/resque-heroku-scaler
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options: []
71
+
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: "0"
86
+ requirements: []
87
+
88
+ rubyforge_project:
89
+ rubygems_version: 1.6.1
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Resque plugin to autoscale Heroku workers
93
+ test_files: []
94
+