heroku-scalr 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/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ heroku-scalr (0.1.0)
5
+ heroku-api (~> 0.3.8)
6
+ timers
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.3.4)
12
+ crack (0.3.2)
13
+ diff-lcs (1.2.4)
14
+ excon (0.20.1)
15
+ heroku-api (0.3.9)
16
+ excon (~> 0.20.1)
17
+ rake (10.0.4)
18
+ rspec (2.13.0)
19
+ rspec-core (~> 2.13.0)
20
+ rspec-expectations (~> 2.13.0)
21
+ rspec-mocks (~> 2.13.0)
22
+ rspec-core (2.13.1)
23
+ rspec-expectations (2.13.0)
24
+ diff-lcs (>= 1.1.3, < 2.0)
25
+ rspec-mocks (2.13.1)
26
+ timers (1.1.0)
27
+ webmock (1.11.0)
28
+ addressable (>= 2.2.7)
29
+ crack (>= 0.3.2)
30
+ yard (0.8.6.1)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ bundler
37
+ heroku-scalr!
38
+ rake
39
+ rspec
40
+ webmock
41
+ yard
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+
3
+ require 'rspec/mocks/version'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ require 'yard'
8
+ YARD::Rake::YardocTask.new
9
+
10
+ desc 'Default: run specs.'
11
+ task :default => :spec
data/bin/heroku-scalr ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require "heroku/scalr/cli"
7
+ Heroku::Scalr::CLI.run!
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.required_ruby_version = '>= 1.9.1'
5
+ s.required_rubygems_version = ">= 1.8.0"
6
+
7
+ s.name = File.basename(__FILE__, '.gemspec')
8
+ s.summary = "Watch and scale your dynos!"
9
+ s.description = "Issues recurring 'pings' to your Heroku apps and scales dynos up or down depending on pre-defined rules"
10
+ s.version = "0.1.0"
11
+
12
+ s.authors = ["Black Square Media"]
13
+ s.email = "info@blacksquaremedia.com"
14
+ s.homepage = "https://github.com/bsm/heroku-scalr"
15
+
16
+ s.require_path = 'lib'
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+
21
+ s.add_dependency "timers"
22
+ s.add_dependency "heroku-api", '~> 0.3.8'
23
+
24
+ s.add_development_dependency "rake"
25
+ s.add_development_dependency "bundler"
26
+ s.add_development_dependency "rspec"
27
+ s.add_development_dependency "yard"
28
+ s.add_development_dependency "webmock"
29
+ end
@@ -0,0 +1,123 @@
1
+ class Heroku::Scalr::App
2
+
3
+ DEFAULTS = {
4
+ interval: 30,
5
+ min_dynos: 1,
6
+ max_dynos: 2,
7
+ wait_low: 10,
8
+ wait_high: 100,
9
+ min_frequency: 60
10
+ }.freeze
11
+
12
+ attr_reader :name, :http, :api, :interval, :min_dynos, :max_dynos,
13
+ :wait_low, :wait_high, :min_frequency, :last_scaled_at
14
+
15
+ # @param [String] name Heroku app name
16
+ # @param [Hash] opts options
17
+ # @option opts [Integer] :interval
18
+ # perform checks every `interval` seconds, default: 60
19
+ # @option opts [Integer] :min_dynos
20
+ # the minimum number of dynos, default: 1
21
+ # @option opts [Integer] :max_dynos
22
+ # the maximum number of dynos, default: 2
23
+ # @option opts [Integer] :wait_low
24
+ # lowers the number of dynos if queue wait time is less than `wait_low` ms, default: 10
25
+ # @option opts [Integer] :wait_high
26
+ # lowers the number of dynos if queue wait time is more than `wait_high` ms, default: 100
27
+ # @option opts [Integer] :min_frequency
28
+ # leave at least `min_frequency` seconds before scaling again, default: 60
29
+ # @option opts [String] :api_key
30
+ # the Heroku account's API key
31
+ def initialize(name, opts = {})
32
+ @name = name.to_s
33
+
34
+ opts = DEFAULTS.merge(opts)
35
+ fail("no API key given") unless opts[:api_key]
36
+ fail("min_dynos must be at least 1") unless opts[:min_dynos] >= 1
37
+ fail("max_dynos must be at least 1") unless opts[:max_dynos] >= 1
38
+ fail("interval must be at least 10") unless opts[:interval] >= 10
39
+
40
+ @http = Excon.new(opts[:url] || "http://#{@name}.herokuapp.com/robots.txt")
41
+ @api = Heroku::API.new api_key: opts[:api_key]
42
+
43
+ @interval = opts[:interval].to_i
44
+ @min_dynos = opts[:min_dynos].to_i
45
+ @max_dynos = opts[:max_dynos].to_i
46
+ @wait_low = opts[:wait_low].to_i
47
+ @wait_high = opts[:wait_high].to_i
48
+ @min_frequency = opts[:min_frequency].to_i
49
+ @last_scaled_at = Time.at(0)
50
+ end
51
+
52
+ # Scales the app
53
+ def scale!
54
+ scale_at = next_scale_attempt
55
+ if Time.now < scale_at
56
+ log :debug, "skip scaling, next attempt in #{(scale_at - Time.now).to_i}s"
57
+ return
58
+ end
59
+
60
+ wait = http.get.headers["X-Heroku-Queue-Wait"]
61
+ unless wait
62
+ log :warn, "unable to determine queue wait time"
63
+ return
64
+ end
65
+
66
+ wait = wait.to_i
67
+ log :debug, "current queue wait time: #{wait}ms"
68
+
69
+ if wait <= wait_low
70
+ do_scale(-1)
71
+ elsif wait >= wait_high
72
+ do_scale(1)
73
+ end
74
+ end
75
+
76
+ # @param [Symbol] level
77
+ # @param [String] message
78
+ def log(level, message)
79
+ Heroku::Scalr.logger.send(level, "[#{name}] #{message}")
80
+ end
81
+
82
+ protected
83
+
84
+ # @return [Time] the next scale attempt
85
+ def next_scale_attempt
86
+ last_scaled_at + min_frequency
87
+ end
88
+
89
+ def do_scale(by)
90
+ info = api.get_app(name)
91
+ unless info.status == 200
92
+ log :warn, "error fetching app info, responded with #{info.status}"
93
+ return
94
+ end
95
+
96
+ current = info.body["dynos"].to_i
97
+ target = current + by
98
+ target = max_dynos if target > max_dynos
99
+ target = min_dynos if target < min_dynos
100
+
101
+ if target == current
102
+ log :debug, "skip scaling, keep #{current} dynos"
103
+ return
104
+ end
105
+
106
+ log :info, "scaling to #{target} dynos"
107
+ result = api.put_dynos(name, target)
108
+ unless result.status == 200
109
+ log :warn, "error scaling app, responded with #{result.status}"
110
+ return
111
+ end
112
+
113
+ @last_scaled_at = Time.now
114
+ target
115
+ end
116
+
117
+ private
118
+
119
+ def fail(message)
120
+ raise ArgumentError, "Invalid options: #{message}"
121
+ end
122
+
123
+ end
@@ -0,0 +1,57 @@
1
+ require 'optparse'
2
+ require 'logger'
3
+
4
+ module Heroku
5
+ module Scalr
6
+ class CLI
7
+
8
+ def self.run!(argv = ARGV)
9
+ new(argv).run!
10
+ end
11
+
12
+ def initialize(argv)
13
+ super()
14
+ @config_path = "./config.rb"
15
+ @options = { log_level: ::Logger::INFO }
16
+ parser.parse!(argv)
17
+
18
+ unless File.file?(@config_path)
19
+ puts parser
20
+ exit
21
+ end
22
+ end
23
+
24
+ def run!
25
+ require 'heroku/scalr'
26
+ Heroku::Scalr.configure(@options)
27
+ Heroku::Scalr.run!(@config_path)
28
+ end
29
+
30
+ def parser
31
+ @parser ||= OptionParser.new do |o|
32
+ o.banner = "Usage: heroku-scalr [options]"
33
+ o.separator ""
34
+
35
+ o.on("-C", "--config PATH", "Configuration file path. Default: ./config.rb") do |path|
36
+ @config_path = path
37
+ end
38
+
39
+ o.on("-l", "--log PATH", "Custom log file path. Default: STDOUT") do |path|
40
+ @options.update log_file: path
41
+ end
42
+
43
+ o.on("-v", "--verbose", "Enable verbose logging.") do
44
+ @options.update log_level: ::Logger::DEBUG
45
+ end
46
+
47
+ o.separator ""
48
+ o.on_tail("-h", "--help", "Show this message") do
49
+ puts o
50
+ exit
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ # Loads a config file and evaluates the stored configuration
2
+ #
3
+ # @example of a config file
4
+ #
5
+ # defaults email: "me@gmail.com", token: "d728201893d47607ec382", interval: 60
6
+ # app "lonely-warrior-45", url: "http://cnamed.host/robots.txt"
7
+ #
8
+ class Heroku::Scalr::Config
9
+
10
+ attr_reader :apps
11
+
12
+ # @param [String] path file path containing a configuration
13
+ def initialize(path)
14
+ @defaults = {}
15
+ @apps = []
16
+ instance_eval File.read(path)
17
+ end
18
+
19
+ # @param [Hash] opts updates for defaults
20
+ # @see Heroku::Scalr::App#initialize for a full set of options
21
+ def defaults(opts = {})
22
+ @defaults.update(opts)
23
+ end
24
+
25
+ # @param [String] name the Heroku app name
26
+ # @param [Hash] opts configuration options
27
+ # @see Heroku::Scalr::App#initialize for a full set of options
28
+ def app(name, opts = {})
29
+ opts = @defaults.merge(opts)
30
+ @apps << Heroku::Scalr::App.new(name, opts)
31
+ end
32
+
33
+ end
@@ -0,0 +1,35 @@
1
+ class Heroku::Scalr::Runner
2
+ SIGNALS = %w[HUP INT TERM] & Signal.list.keys
3
+
4
+ attr_reader :config
5
+
6
+ # @param [String] config_path configuration file location
7
+ def initialize(config_path)
8
+ @config = Heroku::Scalr::Config.new(config_path)
9
+ end
10
+
11
+ # @return [Timers] recurring timers
12
+ def timers
13
+ @timers ||= Timers.new.tap do |t|
14
+ config.apps.each do |app|
15
+ app.log :info, "monitoring every #{app.interval}s"
16
+ t.every(app.interval) { app.scale! }
17
+ end
18
+ end
19
+ end
20
+
21
+ # Start the runner
22
+ def run!
23
+ SIGNALS.each do |sig|
24
+ Signal.trap(sig) { stop! }
25
+ end
26
+ loop { timers.wait }
27
+ end
28
+
29
+ # Stop execution
30
+ def stop!
31
+ Heroku::Scalr.logger.info "Exiting ..."
32
+ exit(0)
33
+ end
34
+
35
+ end
@@ -0,0 +1,32 @@
1
+ require 'heroku/api'
2
+ require 'logger'
3
+ require 'excon'
4
+ require 'timers'
5
+
6
+ module Heroku::Scalr
7
+ extend self
8
+
9
+ # @see Heroku::Scalr::Runner#initialize
10
+ def run!(*args)
11
+ Heroku::Scalr::Runner.new(*args).run!
12
+ end
13
+
14
+ # @param [Hash] opts
15
+ # @options opts [String] :log_file custom log file path
16
+ # @options opts [String] :log_level custom log level
17
+ def configure(opts = {})
18
+ @logger = Logger.new(opts[:log_file]) if opts[:log_file]
19
+ logger.level = opts[:log_level] if opts[:log_level]
20
+ self
21
+ end
22
+
23
+ # @return [Logger] the logger instance
24
+ def logger
25
+ @logger ||= Logger.new(STDOUT)
26
+ end
27
+
28
+ end
29
+
30
+ %w|config app runner|.each do |name|
31
+ require "heroku/scalr/#{name}"
32
+ end
@@ -0,0 +1,9 @@
1
+ defaults api_key: "API_KEY"
2
+
3
+ app "app1",
4
+ max_dynos: 3,
5
+ interval: 180
6
+
7
+ app "app2",
8
+ max_dynos: 4,
9
+ interval: 120
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ describe Heroku::Scalr::App do
4
+
5
+ subject { described_class.new('name', api_key: 'key') }
6
+
7
+ def mock_response(status, body)
8
+ mock "APIResponse", status: status, headers: {}, body: body
9
+ end
10
+
11
+ its(:name) { should == 'name' }
12
+ its(:http) { should be_instance_of(Excon::Connection) }
13
+ its(:api) { should be_instance_of(Heroku::API) }
14
+
15
+ its(:interval) { should be(30) }
16
+ its(:min_dynos) { should be(1) }
17
+ its(:max_dynos) { should be(2) }
18
+ its(:wait_low) { should be(10) }
19
+ its(:wait_high) { should be(100) }
20
+ its(:min_frequency) { should be(60) }
21
+ its(:last_scaled_at) { should == Time.at(0)}
22
+
23
+
24
+ describe "failures" do
25
+ it "should raise error when there's no API key" do
26
+ expect { described_class.new("name") }.to raise_error(ArgumentError)
27
+ end
28
+
29
+ it "should raise error when min_dynos < 1" do
30
+ expect { described_class.new("name", {:api_key => 'key', :min_dynos => 0}) }.to raise_error(ArgumentError)
31
+ end
32
+
33
+ it "should raise error when max_dynos < 1" do
34
+ expect { described_class.new("name", {:api_key => 'key', :max_dynos => 0}) }.to raise_error(ArgumentError)
35
+ end
36
+
37
+ it "should raise error when interval < 10" do
38
+ expect { described_class.new("name", {:api_key => 'key', :interval => 9}) }.to raise_error(ArgumentError)
39
+ end
40
+ end
41
+
42
+ describe "scale!" do
43
+
44
+ context "when low wait time" do
45
+ before do
46
+ subject.api.stub get_app: mock_response(200, { "dynos" => 2 }), put_dynos: mock_response(200, "")
47
+ end
48
+
49
+ let! :app_request do
50
+ stub_request(:get, "http://name.herokuapp.com/robots.txt").
51
+ to_return(body: "", headers: { "X-Heroku-Queue-Wait" => 3 })
52
+ end
53
+
54
+ it "should skip if scaled too recently" do
55
+ subject.instance_variable_set :@last_scaled_at, Time.now
56
+ subject.scale!.should be_nil
57
+ end
58
+
59
+ it "should query the queue wait time from the app" do
60
+ subject.scale!.should == 1
61
+ app_request.should have_been_made
62
+ end
63
+
64
+ it "should check current number of dynos" do
65
+ subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 2 })
66
+ subject.scale!.should == 1
67
+ end
68
+
69
+ it "should update dynos" do
70
+ subject.api.should_receive(:put_dynos).with("name", 1).and_return mock_response(200, "")
71
+ subject.scale!.should == 1
72
+ end
73
+
74
+ it "should not scale down if min number of dynos is reached" do
75
+ subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 1 })
76
+ subject.api.should_not_receive(:put_dynos)
77
+ subject.scale!.should be_nil
78
+ end
79
+ end
80
+
81
+ context "when high wait time" do
82
+ before do
83
+ subject.api.stub get_app: mock_response(200, { "dynos" => 1 }), put_dynos: mock_response(200, "")
84
+ end
85
+
86
+ let! :app_request do
87
+ stub_request(:get, "http://name.herokuapp.com/robots.txt").
88
+ to_return(body: "", headers: { "X-Heroku-Queue-Wait" => 101 })
89
+ end
90
+
91
+ it "should update dynos" do
92
+ subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 1 })
93
+ subject.scale!.should == 2
94
+ end
95
+
96
+ it "should not scale up if max number of dynos is reached" do
97
+ subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 2 })
98
+ subject.api.should_not_receive(:put_dynos)
99
+ subject.scale!.should be_nil
100
+ end
101
+ end
102
+ end
103
+
104
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe Heroku::Scalr::Config do
4
+
5
+ subject { described_class.new(fixture_path("config_a.rb")) }
6
+
7
+ its(:apps) { should be_instance_of(Array) }
8
+ its(:apps) { should have(2).items }
9
+ its(:defaults) { should eq(api_key: "API_KEY") }
10
+
11
+ it 'should merge defaults into app configurations' do
12
+ app = subject.apps.first
13
+ app.should be_instance_of(Heroku::Scalr::App)
14
+ app.api.instance_variable_get(:@api_key).should == "API_KEY"
15
+ end
16
+
17
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Heroku::Scalr::Runner do
4
+
5
+ subject do
6
+ described_class.new fixture_path("config_a.rb")
7
+ end
8
+
9
+ its(:config) { should be_instance_of(Heroku::Scalr::Config) }
10
+ its(:timers) { should be_instance_of(Timers) }
11
+ its(:timers) { should have(2).items }
12
+
13
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Heroku::Scalr do
4
+ end
@@ -0,0 +1,19 @@
1
+ require 'bundler/setup'
2
+ require 'rspec'
3
+ require 'webmock/rspec'
4
+ require 'heroku/scalr'
5
+
6
+ WebMock.disable_net_connect!
7
+
8
+ module Heroku::Scalr::SpecHelper
9
+
10
+ def fixture_path(name)
11
+ File.join File.expand_path("../fixtures", __FILE__), name
12
+ end
13
+
14
+ end
15
+
16
+ RSpec.configure do |c|
17
+ c.before { Heroku::Scalr.logger.stub(:add) }
18
+ c.include Heroku::Scalr::SpecHelper
19
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heroku-scalr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Black Square Media
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: timers
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: heroku-api
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.3.8
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.3.8
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
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
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: yard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: webmock
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Issues recurring 'pings' to your Heroku apps and scales dynos up or down
127
+ depending on pre-defined rules
128
+ email: info@blacksquaremedia.com
129
+ executables:
130
+ - heroku-scalr
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - .travis.yml
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - Rakefile
138
+ - bin/heroku-scalr
139
+ - heroku-scalr.gemspec
140
+ - lib/heroku/scalr.rb
141
+ - lib/heroku/scalr/app.rb
142
+ - lib/heroku/scalr/cli.rb
143
+ - lib/heroku/scalr/config.rb
144
+ - lib/heroku/scalr/runner.rb
145
+ - spec/fixtures/config_a.rb
146
+ - spec/heroku/scalr/app_spec.rb
147
+ - spec/heroku/scalr/config_spec.rb
148
+ - spec/heroku/scalr/runner_spec.rb
149
+ - spec/heroku/scalr_spec.rb
150
+ - spec/spec_helper.rb
151
+ homepage: https://github.com/bsm/heroku-scalr
152
+ licenses: []
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ none: false
159
+ requirements:
160
+ - - ! '>='
161
+ - !ruby/object:Gem::Version
162
+ version: 1.9.1
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ! '>='
167
+ - !ruby/object:Gem::Version
168
+ version: 1.8.0
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 1.8.25
172
+ signing_key:
173
+ specification_version: 3
174
+ summary: Watch and scale your dynos!
175
+ test_files: []
176
+ has_rdoc: